Compare commits

...

8 Commits

Author SHA1 Message Date
a44cc6975d feat: Adds psi to feet of head conversion.
Some checks failed
CI / ubuntu (push) Failing after 7m22s
CI / macOS (debug, 16.2) (push) Has been cancelled
CI / macOS (release, 16.2) (push) Has been cancelled
2025-03-07 16:27:03 -05:00
159031a023 feat: Adds hydronic system pressure calculator.
Some checks are pending
CI / macOS (debug, 16.2) (push) Waiting to run
CI / macOS (release, 16.2) (push) Waiting to run
CI / ubuntu (push) Successful in 6m58s
2025-03-07 15:15:28 -05:00
bdbe89e101 feat: Adds ubuntu tests to ci
Some checks are pending
CI / macOS (debug, 16.2) (push) Waiting to run
CI / macOS (release, 16.2) (push) Waiting to run
CI / ubuntu (push) Successful in 10m16s
2025-03-07 12:01:18 -05:00
ce31efa005 feat: Adds CI tests.
Some checks are pending
CI / macOS (debug, 16.2) (push) Waiting to run
CI / macOS (release, 16.2) (push) Waiting to run
2025-03-07 11:43:24 -05:00
f34e0a8b25 feat: Adds snapshot tests and updates Dockerfile.dev for docker tests. 2025-03-07 11:16:57 -05:00
35a54db9a3 feat: Begins adding html snapshot tests. 2025-03-07 09:08:08 -05:00
b42b9cf03b feat: Adds view controller test files, need to implement snapshot tests, and add test target. 2025-03-06 16:59:56 -05:00
ee577003d5 feat: Adds psychrometrics calculator. 2025-03-06 13:41:38 -05:00
72 changed files with 4042 additions and 138 deletions

32
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
macOS:
name: macOS
runs-on: macOS-latest
strategy:
matrix:
xcode: ['16.2']
config: ['debug', 'release']
steps:
- uses: actions/checkout@v4
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Run ${{ matrix.xcode }} Tests for ${{ matrix.config }}
run: swift test
ubuntu:
name: ubuntu
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build docker test image.
run: docker build -t 'hvac-toolbox:test' -f Dockerfile.dev .
- name: Run tests
run: docker run -i 'hvac-toolbox:test' swift test

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ db.sqlite
!.env.example
.vscode
node_modules
.nvim

View File

@@ -1,6 +1,4 @@
# NOTE: Builds currently fail when building in release mode.
ARG SWIFT_MODE="debug"
ARG SWIFT_MODE="release"
# ================================
# Build image

View File

@@ -1,5 +1,3 @@
# NOTE: Builds currently fail when building in release mode.
ARG SWIFT_MODE="debug"
# ================================
# Build image
@@ -29,70 +27,6 @@ COPY . .
# Build the application, with optimizations, with static linking, and using jemalloc
# N.B.: The static version of jemalloc is incompatible with the static Swift runtime.
RUN --mount=type=cache,target=/build/.build swift build \
-c ${SWIFT_MODE} \
--product App \
--static-swift-stdlib \
-Xswiftc -g
RUN swift build
# Switch to the staging area
WORKDIR /staging
# Copy main executable to staging area
RUN --mount=type=cache,target=/build/.build cp \
"$(swift build --package-path /build -c ${SWIFT_MODE} --show-bin-path)/App" ./
# Copy static swift backtracer binary to staging area
RUN --mount=type=cache,target=/build/.build \
cp "/usr/libexec/swift/linux/swift-backtrace-static" ./
# Copy resources bundled by SPM to staging area
RUN --mount=type=cache,target=/build/.build \
find -L "$(swift build --package-path /build -c ${SWIFT_MODE} --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
# Copy any resources from the public directory and views directory if the directories exist
# Ensure that by default, neither the directory nor any of its contents are writable.
RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true
# ================================
# Run image
# ================================
FROM ubuntu:noble
# Make sure all system packages are up to date, and install only essential packages.
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
&& apt-get -q update \
&& apt-get -q dist-upgrade -y \
&& apt-get -q install -y \
libjemalloc2 \
ca-certificates \
tzdata \
# If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
libcurl4 \
curl \
# If your app or its dependencies import FoundationXML, also install `libxml2`.
# libxml2 \
&& rm -r /var/lib/apt/lists/*
# Create a vapor user and group with /app as its home directory
RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor
# Switch to the new home directory
WORKDIR /app
# Copy built executable and any staged resources from builder
COPY --from=build --chown=vapor:vapor /staging /app
# Provide configuration needed by the built-in crash reporter and some sensible default behaviors.
ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static
# Ensure all further commands run as the vapor user
USER vapor:vapor
# Let Docker bind to port 8080
EXPOSE 8080
# Start the Vapor service when the image is run, default to listening on 8080 in production environment
ENTRYPOINT ["./App"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
CMD ["swift", "test"]

View File

@@ -114,6 +114,17 @@ let package = Package(
.product(name: "ElementaryHTMX", package: "elementary-htmx")
],
swiftSettings: swiftSettings
),
.testTarget(
name: "ViewControllerTests",
dependencies: [
"HTMLSnapshotTesting",
"ViewController"
],
resources: [
.copy("__Snapshots__")
],
swiftSettings: swiftSettings
)
],
swiftLanguageModes: [.v5]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
import Dependencies
import Logging
import PsychrometricClient
import Routes
public extension FeetOfHead.Request {
private static let specificGravity = 1.02
func respond(logger: Logger) async throws -> FeetOfHead.Response {
@Dependency(\.psychrometricClient) var psychrometricClient
try validate()
let waterTemperature = DryBulb.fahrenheit(self.waterTemperature ?? 60)
let density = try await psychrometricClient.density.water(waterTemperature)
let feetOfHead = pressure / ((density.value / 144) * Self.specificGravity)
var warnings = [String]()
if self.waterTemperature == nil {
warnings.append(
"Calculations are based on 60°F water temperature."
)
}
return .init(feetOfHead: feetOfHead, density: density, warnings: warnings)
}
private func validate() throws {
guard pressure > 0 else {
throw ValidationError(message: "Pressure should be greater than 0.")
}
if let waterTemperature {
guard waterTemperature > 32 else {
throw ValidationError(message: "Water temperature should be above freezing.")
}
}
}
}

View File

@@ -0,0 +1,40 @@
import Dependencies
import Logging
import PsychrometricClient
import Routes
public extension HydronicSystemPressure.Request {
func respond(logger: Logger) async throws -> HydronicSystemPressure.Response {
@Dependency(\.psychrometricClient) var psychrometricClient
try validate()
let waterTemperature = DryBulb.fahrenheit(self.waterTemperature ?? 60)
let density = try await psychrometricClient.density.water(waterTemperature)
let pressure = height * (density.value / 144) + 5
var warnings = [String]()
if self.waterTemperature == nil {
warnings.append(
"""
Calculations based on default water temperature of 60°F.
"""
)
}
return .init(pressure: pressure, waterDensity: density, warnings: warnings)
}
private func validate() throws {
guard height > 0 else {
throw ValidationError(message: "Height should be greater than 0.")
}
if let waterTemperature {
guard waterTemperature > 32 else {
throw ValidationError(message: "Water temperature should be above freezing.")
}
}
}
}

View File

@@ -0,0 +1,36 @@
import Dependencies
import Logging
import PsychrometricClient
import Routes
public extension Routes.Psychrometrics.Request {
func respond(logger: Logger) async throws -> Psychrometrics.Response {
@Dependency(\.psychrometricClient) var psychrometricClient
try validate()
let properties = try await psychrometricClient.psychrometricProperties(
.dryBulb(.fahrenheit(temperature), relativeHumidity: humidity%, altitude: .feet(altitude ?? 0))
)
var warnings = [String]()
if altitude == nil || altitude == 0 {
warnings.append(
"Calculations based on altitude of sea level - set project altitude for higher accuracy."
)
}
return .init(properties: properties, warnings: warnings)
}
private func validate() throws {
guard temperature > 0 else {
throw ValidationError(message: "Temperature should be greater than 0.")
}
guard humidity > 0, humidity <= 100 else {
throw ValidationError(message: "Relative humidity should be greater than 0 and less than or equal to 100.")
}
if let altitude, altitude < 0 {
throw ValidationError(message: "Altitude should be greater than 0.")
}
}
}

View File

@@ -4,7 +4,7 @@ public enum AtticVentilation {
Calculate attic ventilation requirements and assess current conditions.
"""
public struct Request: Codable, Equatable, Sendable {
public struct Request: Codable, Equatable, Sendable, Hashable {
public let pressureDifferential: Double
public let outdoorTemperature: Double

View File

@@ -4,17 +4,17 @@ public enum Capacitor {
Calculate run capacitor values based on electrical measurements.
"""
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
case size
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
case test
case size
}
public enum Request: Codable, Equatable, Sendable {
public enum Request: Codable, Equatable, Sendable, Hashable {
case size(SizeRequest)
case test(TestRequest)
public struct SizeRequest: Codable, Equatable, Sendable {
public struct SizeRequest: Codable, Equatable, Sendable, Hashable {
public let runningAmps: Double
public let lineVoltage: Double
@@ -27,7 +27,7 @@ public enum Capacitor {
}
}
public struct TestRequest: Codable, Equatable, Sendable {
public struct TestRequest: Codable, Equatable, Sendable, Hashable {
public let startWindingAmps: Double
public let runToCommonVoltage: Double

View File

@@ -8,7 +8,7 @@ public enum DehumidifierSize {
/// Represents the request for determining dehumidifier size based on
/// latent load and indoor conditions.
public struct Request: Codable, Equatable, Sendable {
public struct Request: Codable, Equatable, Sendable, Hashable {
public let latentLoad: Double
public let temperature: Double

View File

@@ -0,0 +1,74 @@
import CasePaths
import PsychrometricClient
@preconcurrency import URLRouting
public enum FeetOfHead {
public static let description = """
Convert PSI to Feet of Head, to aid in pump flow calculations.
"""
public struct Request: Codable, Equatable, Hashable, Sendable {
public let pressure: Double
public let waterTemperature: Double?
public init(pressure: Double, waterTemperature: Double? = nil) {
self.pressure = pressure
self.waterTemperature = waterTemperature
}
public enum FieldKey: String, CaseIterable {
case pressure
case waterTemperature
}
}
public struct Response: Codable, Equatable, Sendable {
public let feetOfHead: Double
public let density: DensityOf<Water>
public let warnings: [String]
public init(feetOfHead: Double, density: DensityOf<Water>, warnings: [String]) {
self.feetOfHead = feetOfHead
self.density = density
self.warnings = warnings
}
}
}
// MARK: - Router
public extension SiteRoute.View {
enum FeetOfHead: Equatable, Hashable, Sendable {
case index
case submit(Routes.FeetOfHead.Request)
static let rootPath = "feet-of-head"
typealias Key = Routes.FeetOfHead.Request.FieldKey
public static let router = OneOf {
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.submit)) {
Path { rootPath }
Method.post
Body {
FormData {
Field(Key.pressure) { Double.parser() }
Optionally { Field(Key.waterTemperature) { Double.parser() } }
}
.map(.memberwise(Routes.FeetOfHead.Request.init))
}
}
}
}
}
#if DEBUG
public extension FeetOfHead.Response {
static let mock = Self(feetOfHead: 7.95, density: 62.37, warnings: [])
}
#endif

View File

@@ -4,16 +4,16 @@ public enum FilterPressureDrop {
Calculate filter pressure drop and sizing based on system requirements.
"""
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
case basic
case fanLaw
}
public enum Request: Codable, Equatable, Sendable {
public enum Request: Codable, Equatable, Sendable, Hashable {
case basic(Basic)
case fanLaw(FanLaw)
public struct Basic: Codable, Equatable, Sendable {
public struct Basic: Codable, Equatable, Sendable, Hashable {
public let systemSize: Double
public let climateZone: ClimateZone.ZoneType
@@ -36,7 +36,7 @@ public enum FilterPressureDrop {
}
}
public struct FanLaw: Codable, Equatable, Sendable {
public struct FanLaw: Codable, Equatable, Sendable, Hashable {
public let filterWidth: Double
public let filterHeight: Double

View File

@@ -7,7 +7,7 @@ public enum HVACSystemPerformance {
Analyze HVAC system performance and capacity.
"""
public struct Request: Codable, Equatable, Sendable {
public struct Request: Codable, Equatable, Sendable, Hashable {
public let altitude: Double?
public let airflow: Double

View File

@@ -1,4 +1,4 @@
public enum HVACSystemSize: Double, CaseIterable, Codable, Equatable, Sendable {
public enum HVACSystemSize: Double, CaseIterable, Codable, Equatable, Sendable, Hashable {
case one = 1
case oneAndAHalf = 1.5
case two = 2

View File

@@ -6,16 +6,16 @@ public enum HeatingBalancePoint {
Calculate the heating balance point.
"""
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
case economic
case thermal
}
public enum Request: Codable, Equatable, Sendable {
public enum Request: Codable, Equatable, Sendable, Hashable {
case economic(Economic)
case thermal(Thermal)
public struct Economic: Codable, Equatable, Sendable {
public struct Economic: Codable, Equatable, Sendable, Hashable {
public let fuelType: FuelType
public let fuelCostPerUnit: Double
@@ -35,7 +35,7 @@ public enum HeatingBalancePoint {
}
}
public struct Thermal: Codable, Equatable, Sendable {
public struct Thermal: Codable, Equatable, Sendable, Hashable {
public let systemSize: Double
public let capacityAt47: Double?
@@ -119,9 +119,9 @@ public enum HeatingBalancePoint {
}
}
public enum HeatLoss: Codable, Equatable, Sendable {
public enum HeatLoss: Codable, Equatable, Sendable, Hashable {
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
case estimated
case known
}
@@ -137,7 +137,7 @@ public enum HeatingBalancePoint {
}
}
public enum FuelType: String, CaseIterable, Codable, Equatable, Sendable {
public enum FuelType: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
case naturalGas
case propane
case oil

View File

@@ -0,0 +1,83 @@
import CasePaths
import PsychrometricClient
@preconcurrency import URLRouting
public enum HydronicSystemPressure {
public static let description = """
Calculate the required hydronic system pressure based on height of a building.
"""
public struct Request: Codable, Equatable, Hashable, Sendable {
public let height: Double
public let waterTemperature: Double?
public init(height: Double, waterTemperature: Double? = nil) {
self.height = height
self.waterTemperature = waterTemperature
}
public enum FieldKey: String, CaseIterable {
case height
case waterTemperature
}
}
public struct Response: Codable, Equatable, Sendable {
public let pressure: Double
public let waterDensity: DensityOf<Water>
public let warnings: [String]
public init(pressure: Double, waterDensity: DensityOf<Water>, warnings: [String]) {
self.pressure = pressure
self.waterDensity = waterDensity
self.warnings = warnings
}
}
}
// MARK: - Router
public extension SiteRoute.View {
enum HydronicSystemPressure: Equatable, Hashable, Sendable {
case index
case submit(Routes.HydronicSystemPressure.Request)
typealias Key = Routes.HydronicSystemPressure.Request.FieldKey
static let rootPath = "hydronic-system-pressure"
public static let router = OneOf {
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.submit)) {
Path { rootPath }
Method.post
Body {
FormData {
Field(Key.height) { Double.parser() }
Optionally { Field(Key.waterTemperature) { Double.parser() } }
}
.map(.memberwise(Routes.HydronicSystemPressure.Request.init))
}
}
}
}
}
#if DEBUG
public extension HydronicSystemPressure.Response {
static let mock = Self(
pressure: 15,
waterDensity: 62.37,
warnings: [
"Water density based on 60 water - include water temperature for more accurate result."
]
)
}
#endif

View File

@@ -7,7 +7,7 @@ public enum MoldRisk {
Assess mold risk based on indoor conditions.
"""
public struct Request: Codable, Equatable, Sendable {
public struct Request: Codable, Equatable, Sendable, Hashable {
public let temperature: Double
public let humidity: Double

View File

@@ -0,0 +1,107 @@
import CasePaths
import PsychrometricClient
@preconcurrency import URLRouting
public enum Psychrometrics {
public static let description = """
Calculate the psychrometric properties of air based on temperature and humidity readings.
"""
public struct Request: Codable, Equatable, Sendable, Hashable {
// public let mode: Mode
public let temperature: Double
public let humidity: Double
public let altitude: Double?
public init(temperature: Double, humidity: Double, altitude: Double? = nil) {
self.temperature = temperature
self.humidity = humidity
self.altitude = altitude
}
public enum FieldKey: String {
case temperature
case humidity
case altitude
}
}
public struct Response: Codable, Equatable, Sendable {
public let properties: PsychrometricProperties
public let warnings: [String]
public init(properties: PsychrometricProperties, warnings: [String]) {
self.properties = properties
self.warnings = warnings
}
}
}
// MARK: - Router
public extension SiteRoute.View {
enum Psychrometrics: Equatable, Sendable, Hashable {
case index
case submit(Routes.Psychrometrics.Request)
typealias Key = Routes.Psychrometrics.Request.FieldKey
static let rootPath = "psychrometric-properties"
public static let router = OneOf {
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.submit)) {
Path { rootPath }
Method.post
Body {
FormData {
Field(Key.temperature) {
Double.parser()
}
Field(Key.humidity) {
Double.parser()
}
Optionally {
Field(Key.altitude) {
Double.parser()
}
}
}
}
.map(.memberwise(Routes.Psychrometrics.Request.init))
}
}
}
}
#if DEBUG
public extension Psychrometrics.Response {
static let mock = Self(
properties: .init(
absoluteHumidity: 91.4,
atmosphericPressure: 0.3,
degreeOfSaturation: 0.66,
density: 0.07,
dewPoint: 64.3,
dryBulb: 76,
enthalpy: 32.33,
grainsOfMoisture: 91.4,
humidityRatio: 0.01,
relativeHumidity: 67,
specificVolume: 13.78,
vaporPressure: 0.3,
wetBulb: 67.7,
units: .imperial
),
warnings: [
"Test warning."
]
)
}
#endif

View File

@@ -4,7 +4,7 @@ public enum RoomPressure {
Calculate return grille and duct sizing for room pressure balancing.
"""
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
case knownAirflow
case measuredPressure
@@ -16,11 +16,11 @@ public enum RoomPressure {
}
}
public enum Request: Codable, Equatable, Sendable {
public enum Request: Codable, Equatable, Sendable, Hashable {
case knownAirflow(KnownAirflow)
case measuredPressure(MeasuredPressure)
public struct KnownAirflow: Codable, Equatable, Sendable {
public struct KnownAirflow: Codable, Equatable, Sendable, Hashable {
public let targetRoomPressure: Double
public let doorWidth: Double
@@ -46,7 +46,7 @@ public enum RoomPressure {
}
}
public struct MeasuredPressure: Codable, Equatable, Sendable {
public struct MeasuredPressure: Codable, Equatable, Sendable, Hashable {
public let measuredRoomPressure: Double // pascals.
public let doorWidth: Double

View File

@@ -4,6 +4,9 @@ import Foundation
import PsychrometricClient
@preconcurrency import URLRouting
// FIX: Move Routers into their respective model files, to reduce the size / complexity of this file
// and keep them closer to where changes may need made.
// swiftlint:disable type_body_length
public enum SiteRoute: Equatable, Sendable {
@@ -95,16 +98,19 @@ public enum SiteRoute: Equatable, Sendable {
// }
public extension SiteRoute {
enum View: Equatable, Sendable {
enum View: Equatable, Sendable, Hashable {
case index
case atticVentilation(AtticVentilation)
case capacitor(Capacitor)
case dehumidifierSize(DehumidifierSize)
case feetOfHead(Self.FeetOfHead)
case filterPressureDrop(FilterPressureDrop)
case heatingBalancePoint(HeatingBalancePoint)
case hvacSystemPerformance(HVACSystemPerformance)
case hydronicSystemPressure(Self.HydronicSystemPressure)
case moldRisk(MoldRisk)
case psychrometrics(Self.Psychrometrics)
case roomPressure(RoomPressure)
public static let router = OneOf {
@@ -120,6 +126,9 @@ public extension SiteRoute {
Route(.case(Self.dehumidifierSize)) {
DehumidifierSize.router
}
Route(.case(Self.feetOfHead)) {
FeetOfHead.router
}
Route(.case(Self.filterPressureDrop)) {
FilterPressureDrop.router
}
@@ -129,15 +138,21 @@ public extension SiteRoute {
Route(.case(Self.hvacSystemPerformance)) {
HVACSystemPerformance.router
}
Route(.case(Self.hydronicSystemPressure)) {
HydronicSystemPressure.router
}
Route(.case(Self.moldRisk)) {
MoldRisk.router
}
Route(.case(Self.psychrometrics)) {
Self.Psychrometrics.router
}
Route(.case(Self.roomPressure)) {
RoomPressure.router
}
}
public enum AtticVentilation: Equatable, Sendable {
public enum AtticVentilation: Equatable, Sendable, Hashable {
case index
case submit(Routes.AtticVentilation.Request)
@@ -168,7 +183,7 @@ public extension SiteRoute {
}
}
public enum Capacitor: Equatable, Sendable {
public enum Capacitor: Equatable, Sendable, Hashable {
case index(mode: Routes.Capacitor.Mode? = nil)
case submit(Routes.Capacitor.Request)
@@ -212,7 +227,7 @@ public extension SiteRoute {
}
}
public enum DehumidifierSize: Equatable, Sendable {
public enum DehumidifierSize: Equatable, Sendable, Hashable {
case index
case submit(Routes.DehumidifierSize.Request)
@@ -238,7 +253,7 @@ public extension SiteRoute {
}
}
public enum FilterPressureDrop: Equatable, Sendable {
public enum FilterPressureDrop: Equatable, Sendable, Hashable {
case index(mode: Routes.FilterPressureDrop.Mode? = nil)
case submit(Routes.FilterPressureDrop.Request)
@@ -285,7 +300,7 @@ public extension SiteRoute {
}
}
public enum HeatingBalancePoint: Equatable, Sendable {
public enum HeatingBalancePoint: Equatable, Sendable, Hashable {
case index(
mode: Routes.HeatingBalancePoint.Mode? = nil,
heatLossMode: Routes.HeatingBalancePoint.HeatLoss.Mode? = nil
@@ -350,7 +365,7 @@ public extension SiteRoute {
}
}
public enum HVACSystemPerformance: Equatable, Sendable {
public enum HVACSystemPerformance: Equatable, Sendable, Hashable {
case index
case submit(Routes.HVACSystemPerformance.Request)
@@ -380,7 +395,7 @@ public extension SiteRoute {
}
}
public enum MoldRisk: Equatable, Sendable {
public enum MoldRisk: Equatable, Sendable, Hashable {
case index
case submit(Routes.MoldRisk.Request)
@@ -410,7 +425,7 @@ public extension SiteRoute {
}
}
public enum RoomPressure: Equatable, Sendable {
public enum RoomPressure: Equatable, Sendable, Hashable {
case index(mode: Routes.RoomPressure.Mode? = nil)
case submit(Routes.RoomPressure.Request)
@@ -460,6 +475,7 @@ public extension SiteRoute {
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
import URLRouting
// Allow the use of a field key enum as the `name` parameter, to avoid
// stringly type name fields.
extension Field {
@inlinable
init<Key>(
_ name: Key,
default defaultValue: Value.Output? = nil,
@ParserBuilder<Substring> _ value: () -> Value
) where Key: RawRepresentable, Key.RawValue == String {
self.init(name.rawValue, default: defaultValue, value)
}
// @inlinable
// init<Key, C>(
// _ name: Key,
// _ value: C,
// default defaultValue: Value.Output? = nil,
// ) where Key: RawRepresentable, Key.RawValue == String,
// Value == Parsers.MapConversion<Parsers.ReplaceError<Rest<Substring>>, C>
// {
// self.init(name.rawValue, value, default: defaultValue)
// }
//
// @inlinable
// init<Key>(
// _ name: Key,
// default defaultValue: Value.Output? = nil
// )
// where
// Key: RawRepresentable, Key.RawValue == String,
// Value == Parsers.MapConversion<
// Parsers.ReplaceError<Rest<Substring>>, Conversions.SubstringToString
// >
// {
// self.init(name.rawValue, default: defaultValue)
// }
}

View File

@@ -59,6 +59,17 @@ public struct Input: HTML, Sendable {
}
}
public extension Input {
init<Key>(
id: Key,
name: String? = nil,
placeholder: String
) where Key: RawRepresentable, Key.RawValue == String {
self.init(id: id.rawValue, name: name, placeholder: placeholder)
}
}
/// A style form input label.
public struct InputLabel<InputLabel: HTML>: HTML {

View File

@@ -0,0 +1,17 @@
import Elementary
public struct ResultBox<Body: HTML>: HTML {
let body: Body
public init(@HTMLBuilder body: () -> Body) {
self.body = body()
}
public var content: some HTML<HTMLTag.div> {
div(.class("w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6")) {
body
}
}
}
extension ResultBox: Sendable where Body: Sendable {}

View File

@@ -0,0 +1,18 @@
import Elementary
// A rounded box, with no color styles. Colors should be added at call site.
public struct Box<Body: HTML>: HTML {
let body: Body
public init(@HTMLBuilder body: () -> Body) {
self.body = body()
}
public var content: some HTML<HTMLTag.div> {
div(.class("w-full rounded-lg shadow-lgp-6")) {
body
}
}
}
extension Box: Sendable where Body: Sendable {}

View File

@@ -45,8 +45,10 @@ public struct SVGSize: Sendable {
public enum SVGType: Sendable, CaseIterable {
case calculator
case checkCircle
case circleGauge
case droplets
case exclamation
case footprints
case funnel
case house
case leftRightArrow
@@ -63,8 +65,10 @@ public enum SVGType: Sendable, CaseIterable {
switch self {
case .calculator: return calculatorSvg(size: size)
case .checkCircle: return checkCircleSvg(size: size)
case .circleGauge: return circleGaugeSvg(size: size)
case .droplets: return dropletsSvg(size: size)
case .exclamation: return exclamationSvg(size: size)
case .footprints: return footprintsSvg(size: size)
case .funnel: return funnelSvg(size: size)
case .house: return houseSvg(size: size)
case .leftRightArrow: return leftRightArrowSvg(size: size)
@@ -84,6 +88,27 @@ public enum SVGType: Sendable, CaseIterable {
// swiftlint:disable line_length
private func footprintsSvg(size: SVGSize) -> HTMLRaw {
HTMLRaw("""
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-footprints">
<path d="M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z"/>
<path d="M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z"/>
<path d="M16 17h4"/>
<path d="M4 13h4"/>
</svg>
""")
}
private func circleGaugeSvg(size: SVGSize) -> HTMLRaw {
HTMLRaw("""
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-gauge">
<path d="M15.6 2.7a10 10 0 1 0 5.7 5.7"/>
<circle cx="12" cy="12" r="2"/>
<path d="M13.4 10.6 19 5"/>
</svg>
""")
}
private func scaleSvg(size: SVGSize) -> HTMLRaw {
HTMLRaw("""
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">

View File

@@ -0,0 +1,3 @@
public extension String {
static let fahrenheit = "°F"
}

View File

@@ -55,6 +55,24 @@ struct HomePage: HTML, Sendable {
svg: .scale,
route: .heatingBalancePoint(.index)
)
group(
label: "Psychrometrics",
description: Psychrometrics.description,
svg: .droplets,
route: .psychrometrics(.index)
)
group(
label: "Hydronic System Pressure",
description: HydronicSystemPressure.description,
svg: .circleGauge,
route: .hydronicSystemPressure(.index)
)
group(
label: "Feet of Head",
description: FeetOfHead.description,
svg: .footprints,
route: .feetOfHead(.index)
)
}
}

View File

@@ -100,6 +100,15 @@ extension ViewController: DependencyKey {
return DehumidifierSizeResult(response: response)
}
case let .feetOfHead(route):
switch route {
case .index:
return request.respond(FeetOfHeadForm(response: nil))
case let .submit(request):
let response = try await request.respond(logger: logger)
return FeetOfHeadResponse(response: response)
}
case let .filterPressureDrop(route):
switch route {
case let .index(mode: mode):
@@ -127,13 +136,21 @@ extension ViewController: DependencyKey {
case let .hvacSystemPerformance(route):
switch route {
case .index:
// let response = try await HVACSystemPerformance.Response.mock()
return request.respond(HVACSystemPerformanceForm(response: nil))
case let .submit(hvacRequest):
let response = try await hvacRequest.respond(logger: request.logger)
return HVACSystemPerformanceResult(response: response)
}
case let .hydronicSystemPressure(route):
switch route {
case .index:
return request.respond(HydronicSystemPressureForm(response: nil))
case let .submit(request):
let response = try await request.respond(logger: logger)
return HydronicSystemPressureResponse(response: response)
}
case let .moldRisk(route):
switch route {
case .index:
@@ -143,6 +160,15 @@ extension ViewController: DependencyKey {
return MoldRiskResponse(response: response)
}
case let .psychrometrics(route):
switch route {
case .index:
return request.respond(PsychrometricsForm(response: nil))
case let .submit(request):
let response = try await request.respond(logger: logger)
return PsychrometricsResponse(response: response)
}
case let .roomPressure(route):
switch route {
case let .index(mode):

View File

@@ -0,0 +1,70 @@
import Elementary
import ElementaryHTMX
import PsychrometricClient
import Routes
import Styleguide
struct FeetOfHeadForm: HTML, Sendable {
let response: FeetOfHead.Response?
typealias Key = FeetOfHead.Request.FieldKey
var content: some HTML {
FormHeader(label: "Feet of Head", svg: .footprints)
form(
.hx.post(route: .feetOfHead(.index)),
.hx.target("#result")
) {
div(.class("space-y-6")) {
div(.class("grid grid-cols-2 gap-4")) {
LabeledContent(label: "Pressure (psi)") {
Input(id: Key.pressure, placeholder: "Pressure")
.attributes(.type(.number), .min("0.1"), .step("0.1"), .autofocus, .required)
}
LabeledContent(label: "Water Temperature (\(String.fahrenheit))") {
Input(id: Key.waterTemperature, placeholder: "Temperature (optional)")
.attributes(.type(.number), .min("32.0"), .step("0.1"))
}
}
Note {
"""
If water temperature is not supplied, then calculations will be based on 60\(String.fahrenheit) water.
"""
}
div {
SubmitButton(label: "Feet of Head")
}
}
}
div(.id("result")) {
if let response {
FeetOfHeadResponse(response: response)
}
}
}
}
struct FeetOfHeadResponse: HTML, Sendable {
let response: FeetOfHead.Response
var content: some HTML {
ResultContainer(reset: .feetOfHead(.index)) {
ResultBox {
div(.class("grid grid-cols-2")) {
VerticalGroup(label: "Feet of Head", value: "\(double: response.feetOfHead)", valueLabel: "ft.")
VerticalGroup(
label: "Water Density",
value: "\(double: response.density.value)",
valueLabel: response.density.units.rawValue
)
}
}
WarningBox(warnings: response.warnings)
}
}
}

View File

@@ -0,0 +1,74 @@
import Elementary
import ElementaryHTMX
import PsychrometricClient
import Routes
import Styleguide
struct HydronicSystemPressureForm: HTML, Sendable {
let response: HydronicSystemPressure.Response?
typealias Key = HydronicSystemPressure.Request.FieldKey
var content: some HTML {
FormHeader(label: "Hydronic System Pressure", svg: .circleGauge)
form(
.hx.post(route: .hydronicSystemPressure(.index)),
.hx.target("#result")
) {
div(.class("space-y-6")) {
div(.class("grid grid-cols-2 gap-4")) {
LabeledContent(label: "Height (ft.)") {
Input(id: Key.height, placeholder: "Building height")
.attributes(.type(.number), .min("0.1"), .step("0.1"), .autofocus, .required)
}
LabeledContent(label: "Water Temperature (\(String.fahrenheit))") {
Input(id: Key.waterTemperature, placeholder: "Temperature (optional)")
.attributes(.type(.number), .min("32.0"), .step("0.1"))
}
}
Note {
"""
Water temperature should be the coldest water temperature the system sees, which for boilers will be
when the system is filled with water.
"""
}
div {
SubmitButton(label: "Calculate System Pressure")
}
}
}
div(.id("result")) {
if let response {
HydronicSystemPressureResponse(response: response)
}
}
}
}
struct HydronicSystemPressureResponse: HTML, Sendable {
let response: HydronicSystemPressure.Response
var content: some HTML {
ResultContainer(reset: .hydronicSystemPressure(.index)) {
ResultBox {
div(.class("grid grid-cols-2")) {
VerticalGroup(label: "Pressure", value: "\(double: response.pressure)", valueLabel: "psi")
VerticalGroup(
label: "Water Density",
value: "\(double: response.waterDensity.value)",
valueLabel: response.waterDensity.units.rawValue
)
}
}
WarningBox(warnings: response.warnings)
Note {
"Expansion tank pressure should match system fill pressure."
}
}
}
}

View File

@@ -1,6 +1,7 @@
import Elementary
import PsychrometricClient
// FIX: Remove text colors.
struct PsychrometricPropertiesView: HTML {
let properties: PsychrometricProperties

View File

@@ -0,0 +1,126 @@
import Elementary
import ElementaryHTMX
import PsychrometricClient
import Routes
import Styleguide
struct PsychrometricsForm: HTML, Sendable {
// let mode: Psychrometrics.Mode
let response: Psychrometrics.Response?
init(response: Psychrometrics.Response? = nil) {
self.response = response
}
var content: some HTML {
FormHeader(label: "Psychrometric Properties", svg: .droplets)
form(
.hx.post(route: .psychrometrics(.index)),
.hx.target("#result")
) {
div(.class("space-y-6")) {
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) {
LabeledContent(label: "Temperature (°F)") {
Input(id: Psychrometrics.Request.FieldKey.temperature.rawValue, placeholder: "Dry bulb temperature")
.attributes(.type(.number), .min("0.1"), .step("0.1"), .autofocus, .required)
}
LabeledContent(label: "Relative Humidity (%)") {
Input(id: Psychrometrics.Request.FieldKey.humidity.rawValue, placeholder: "Relative humidity")
.attributes(.type(.number), .min("0.1"), .step("0.1"), .max("100"), .required)
}
}
LabeledContent(label: "Altitude (ft.)") {
Input(id: Psychrometrics.Request.FieldKey.altitude.rawValue, placeholder: "Altitude (optional)")
.attributes(.type(.number), .min("0.1"), .step("0.1"))
}
div {
SubmitButton(label: "Calculate Psychrometrics")
}
}
}
div(.id("result")) {
if let response {
PsychrometricsResponse(response: response)
}
}
}
}
struct PsychrometricsResponse: HTML, Sendable {
let response: Psychrometrics.Response
var content: some HTML {
ResultContainer(reset: .psychrometrics(.index)) {
div(.class("w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6")) {
div(.class("flex mb-8")) {
SVG(.droplets, color: "text-blue-600")
h3(.class("text-xl px-2 font-semibold")) { "Psychrometrics" }
}
div(.class("grid grid-cols-3 gap-4")) {
div(.class("rounded-lg bg-purple-200 border border-purple-600 text-purple-600")) {
VerticalGroup(
label: "Dew Point",
value: "\(double: response.properties.dewPoint.value, fractionDigits: 1)",
valueLabel: response.properties.dewPoint.units.symbol
)
.attributes(.class("my-8"))
}
div(.class("rounded-lg bg-orange-200 border border-orange-600 text-orange-600")) {
VerticalGroup(
label: "Enthalpy",
value: "\(double: response.properties.enthalpy.value, fractionDigits: 1)",
valueLabel: response.properties.enthalpy.units.rawValue
)
.attributes(.class("my-8"))
}
div(.class("rounded-lg bg-green-200 border border-green-600 text-green-600")) {
VerticalGroup(
label: "Wet Bulb",
value: "\(double: response.properties.wetBulb.value, fractionDigits: 1)",
valueLabel: response.properties.wetBulb.units.symbol
)
.attributes(.class("my-8"))
}
}
div(.class("mt-8")) {
h4(.class("text-lg font-semibold")) { "Other Properties" }
div(.class("rounded-lg border border-blue-300")) {
displayProperty("Density", \.density.rawValue)
displayProperty("Vapor Pressure", \.vaporPressure.rawValue)
displayProperty("Specific Volume", response.properties.specificVolume.rawValue)
displayProperty("Absolute Humidity", \.absoluteHumidity)
displayProperty("Humidity Ratio", response.properties.humidityRatio.value)
displayProperty("Degree of Saturation", response.properties.degreeOfSaturation.value)
}
}
}
WarningBox(warnings: response.warnings)
}
}
private func displayProperty(_ label: String, _ value: Double, _ symbol: String? = nil) -> some HTML {
let symbol = "\(symbol == nil ? "" : " \(symbol!)")"
return div(.class("flex items-center justify-between border-b border-blue-300 p-2")) {
span(.class("font-semibold")) { "\(label): " }
span(.class("font-light")) {
"\(double: value, fractionDigits: 2)\(symbol)"
}
}
}
private func displayProperty<N: NumberWithUnitOfMeasure>(
_ label: String,
_ keyPath: KeyPath<PsychrometricProperties, N>
) -> some HTML where N.Number == Double, N.Units: RawRepresentable, N.Units.RawValue == String {
let property = response.properties[keyPath: keyPath]
return displayProperty(label, property.rawValue, property.units.rawValue)
}
}

View File

@@ -1,29 +1,29 @@
@testable import App
import VaporTesting
import Testing
@Suite("App Tests")
struct AppTests {
private func withApp(_ test: (Application) async throws -> ()) async throws {
let app = try await Application.make(.testing)
do {
try await configure(app)
try await test(app)
}
catch {
try await app.asyncShutdown()
throw error
}
try await app.asyncShutdown()
}
@Test("Test Hello World Route")
func helloWorld() async throws {
try await withApp { app in
try await app.testing().test(.GET, "hello", afterResponse: { res async in
#expect(res.status == .ok)
#expect(res.body.string == "Hello, world!")
})
}
}
}
// @testable import App
// import VaporTesting
// import Testing
//
// @Suite("App Tests")
// struct AppTests {
// private func withApp(_ test: (Application) async throws -> ()) async throws {
// let app = try await Application.make(.testing)
// do {
// try await configure(app)
// try await test(app)
// }
// catch {
// try await app.asyncShutdown()
// throw error
// }
// try await app.asyncShutdown()
// }
//
// @Test("Test Hello World Route")
// func helloWorld() async throws {
// try await withApp { app in
// try await app.testing().test(.GET, "hello", afterResponse: { res async in
// #expect(res.status == .ok)
// #expect(res.body.string == "Hello, world!")
// })
// }
// }
// }

View File

@@ -0,0 +1,157 @@
import Dependencies
import Elementary
import HTMLSnapshotTesting
import Logging
import OrderedCollections
import PsychrometricClientLive
import Routes
import SnapshotTesting
import Testing
@testable import ViewController
@Suite("ViewControllerTests")
struct ViewControllerTests {
let record = SnapshotTestingConfiguration.Record.missing
let logger = Logger(label: "ViewControllerTests")
// swiftlint:disable function_body_length
@Test
func snapShotTests() async throws {
try await withSnapshotTesting(record: record) {
try await withDependencies {
$0.viewController = .liveValue
$0.psychrometricClient = .liveValue
} operation: {
@Dependency(\.viewController) var viewController
let arguments = OrderedSet([
// Attic ventilation
SiteRoute.View.atticVentilation(.index),
.atticVentilation(.submit(.init(
pressureDifferential: 1,
outdoorTemperature: 76,
outdoorDewpoint: 67,
atticTemperature: 76,
atticDewpoint: 76,
atticFloorArea: 1234
))),
// Capacitor
.capacitor(.index),
.capacitor(.index(mode: .size)),
.capacitor(.index(mode: .test)),
.capacitor(.submit(.size(.init(runningAmps: 10.7, lineVoltage: 243, powerFactor: 0.86)))),
.capacitor(.submit(.test(.init(startWindingAmps: 4.3, runToCommonVoltage: 343)))),
// Dehumidifier Sizing
.dehumidifierSize(.index),
.dehumidifierSize(.submit(.init(latentLoad: 3443, temperature: 76, humidity: 67))),
// Filter Pressure Drop
.filterPressureDrop(.index),
.filterPressureDrop(.index(mode: .basic)),
.filterPressureDrop(.index(mode: .fanLaw)),
.filterPressureDrop(.submit(.basic(.init(
systemSize: 2,
climateZone: .hotHumid,
filterType: .pleatedBest,
filterWidth: 20,
filterHeight: 25
)))),
.filterPressureDrop(.submit(.fanLaw(.init(
filterWidth: 20,
filterHeight: 25,
filterDepth: 4,
ratedAirflow: 800,
ratedPressureDrop: 0.1,
designAirflow: 900
)))),
// Heating Balance Point
.heatingBalancePoint(.index),
.heatingBalancePoint(.heatLossFields(mode: .estimated)),
.heatingBalancePoint(.heatLossFields(mode: .known)),
.heatingBalancePoint(.submit(.economic(.init(
fuelType: .propane,
fuelCostPerUnit: 2.43,
fuelAFUE: 90,
costPerKW: 0.13
)))),
.heatingBalancePoint(.submit(.thermal(.init(
systemSize: 2,
capacityAt47: 24600,
capacityAt17: 15100,
heatingDesignTemperature: 5,
buildingHeatLoss: .known(btu: 45667),
climateZone: .five
)))),
// HVAC System Performance
.hvacSystemPerformance(.index),
.hvacSystemPerformance(.submit(.init(
altitude: 800,
airflow: 800,
returnAirTemperature: 76,
returnAirHumidity: 67,
supplyAirTemperature: 56,
supplyAirHumidity: 89,
systemSize: 2
))),
// Mold risk
.moldRisk(.index),
.moldRisk(.submit(.init(temperature: 76, humidity: 67))),
// Psychrometrics
.psychrometrics(.index),
.psychrometrics(.submit(.init(temperature: 76, humidity: 67, altitude: 800))),
// Room pressures
.roomPressure(.index),
.roomPressure(.index(mode: .measuredPressure)),
.roomPressure(.submit(.knownAirflow(.init(
targetRoomPressure: 3,
doorWidth: 30,
doorHeight: 86,
doorUndercut: 1,
supplyAirflow: 200,
preferredGrilleHeight: .fourteen
)))),
.roomPressure(.submit(.measuredPressure(.init(
measuredRoomPressure: 4,
doorWidth: 30,
doorHeight: 86,
doorUndercut: 1,
preferredGrilleHeight: .fourteen
)))),
// Hydronic system pressure
.hydronicSystemPressure(.index),
.hydronicSystemPressure(.submit(.init(height: 12, waterTemperature: 60))),
// Feet of head
.feetOfHead(.index),
.feetOfHead(.submit(.init(pressure: 3.5, waterTemperature: 60)))
])
for route in arguments {
let html = try await viewController.render(route, logger: logger)
assertSnapshot(of: html, as: .html)
}
}
}
}
// swiftlint:enable function_body_length
}
private extension ViewController {
func render(_ route: SiteRoute.View, logger: Logger) async throws -> String {
let html = try await view(.init(route, isHtmxRequest: true, logger: logger))
return html.renderFormatted()
}
}

View File

@@ -0,0 +1,92 @@
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-8 h-8">
<path d="M17.7 7.7a2.5 2.5 0 1 1 1.8 4.3H2"></path>
<path d="M9.6 4.6A2 2 0 1 1 11 8H2"></path>
<path d="M12.6 19.4A2 2 0 1 0 14 16H2"></path>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Attic Ventilation Calculator</h2>
</div>
<form hx-post="/attic-ventilation" hx-target="#result">
<div class="space-y-6">
<div>
<div>
<label for="pressureDifferential" class="block text-sm font-medium mb-2">Pressure Differential: (Pascals - WRT outside)</label>
<input id="pressureDifferential" placeholder="Pressure differential" name="pressureDifferential" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" autofocus required>
</div>
<p class="text-sm text-light mt-2">Positive values indicate higher attic pressure</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p class="font-bold mb-6">Outdoor Conditions</p>
<div>
<label for="outdoorTemperature" class="block text-sm font-medium mb-2">Temperature: (°F)</label>
<input id="outdoorTemperature" placeholder="Outdoor temperature" name="outdoorTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500 mb-4" type="number" step="0.1" required>
</div>
<div>
<label for="outdoorDewpoint" class="block text-sm font-medium mb-2">Dew Point: (°F)</label>
<input id="outdoorDewpoint" placeholder="Outdoor dewpoint" name="outdoorDewpoint" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" required>
</div>
</div>
<div>
<p class="font-bold mb-6">Attic Conditions</p>
<div>
<label for="atticTemperature" class="block text-sm font-medium mb-2">Temperature: (°F)</label>
<input id="atticTemperature" placeholder="Attic temperature" name="atticTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500 mb-4" type="number" step="0.1" required>
</div>
<div>
<label for="atticDewpoint" class="block text-sm font-medium mb-2">Dew Point: (°F)</label>
<input id="atticDewpoint" placeholder="Attic dewpoint" name="atticDewpoint" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" required>
</div>
</div>
</div>
<div>
<label for="atticFloorArea" class="block text-sm font-medium mb-2">Attic Floor Area: (ft²)</label>
<input id="atticFloorArea" placeholder="Attic floor area" name="atticFloorArea" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="existingIntakeArea" class="block text-sm font-medium mb-2">Existing Intake Area: (ft²)</label>
<input id="existingIntakeArea" placeholder="Intake area (optional)" name="existingIntakeArea" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500 mb-4" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="existingExhaustArea" class="block text-sm font-medium mb-2">Existing Exhaust Area: (ft²)</label>
<input id="existingExhaustArea" placeholder="Exhaust area (optional)" name="existingExhaustArea" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Ventilation</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,90 @@
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g clip-path="url(#clip0_429_11128)">
<path d="M20 3.99994H4L9.6 11.4666C9.85964 11.8128 10 12.2339 10 12.6666V19.9999L14 17.9999V12.6666C14 12.2339 14.1404 11.8128 14.4 11.4666L20 3.99994Z" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"></path>
</g>
<defs>
<clipPath id="clip0_429_11128"> <rect width="24" height="24" fill="white"></rect>
</clipPath>
</defs>
</g>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Filter Pressure Drop - Basic</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-s-lg" disabled type="button">Basic</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/filter-pressure-drop?mode=fanLaw" type="button">Fan Law</button>
</div>
</div>
<form hx-post="/filter-pressure-drop" hx-target="#result">
<div class="space-y-6">
<div>
<div>
<h4 class="text-lg font-bold mb-4">System Parameters</h4>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6">
<div>
<label for="systemSize" class="block text-sm font-medium mb-2">System Size: (Tons)</label>
<input id="systemSize" placeholder="Tons" name="systemSize" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5" min="1.0" autofocus required>
</div>
<div>
<label for="climateZone" class="block text-sm font-medium mb-2">Climate Zone</label>
<select id="climateZone" name="climateZone" class="w-full rounded-md border px-4 py-2 min-h-11">
<option value="hotHumid">Hot Humid (1A, 2A)</option>
<option value="moist">Moist (3A, 4A, 5A, 6A, 7A)</option>
<option value="dry">Dry (2B, 3B, 4B, 5B, 6B, 7B)</option>
<option value="marine">Marine (3C, 4C)</option>
</select>
</div>
<div>
<label for="filterType" class="block text-sm font-medium mb-2">Filter Type</label>
<select id="filterType" name="filterType" class="w-full rounded-md border px-4 py-2 min-h-11">
<option value="fiberglass">Fiberglass (MERV 1-4)</option>
<option value="pleatedBasic">Basic Pleated (MERV 5-8)</option>
<option value="pleatedBetter">Better Pleated (MERV 9-12)</option>
<option value="pleatedBest">Best Pleated (MERV 13-16)</option>
</select>
</div>
</div>
</div>
<div class="mt-6 lg:mt-0">
<h4 class="text-lg font-bold mb-4">Filter Parameters</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-4 space-y-6">
<div>
<label for="filterWidth" class="block text-sm font-medium mb-2">Width: (in)</label>
<input id="filterWidth" placeholder="Width" name="filterWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="filterHeight" class="block text-sm font-medium mb-2">Height: (in)</label>
<input id="filterHeight" placeholder="Height" name="filterHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
</div>
</div>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Pressure Drop</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,90 @@
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g clip-path="url(#clip0_429_11128)">
<path d="M20 3.99994H4L9.6 11.4666C9.85964 11.8128 10 12.2339 10 12.6666V19.9999L14 17.9999V12.6666C14 12.2339 14.1404 11.8128 14.4 11.4666L20 3.99994Z" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"></path>
</g>
<defs>
<clipPath id="clip0_429_11128"> <rect width="24" height="24" fill="white"></rect>
</clipPath>
</defs>
</g>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Filter Pressure Drop - Basic</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-s-lg" disabled type="button">Basic</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/filter-pressure-drop?mode=fanLaw" type="button">Fan Law</button>
</div>
</div>
<form hx-post="/filter-pressure-drop" hx-target="#result">
<div class="space-y-6">
<div>
<div>
<h4 class="text-lg font-bold mb-4">System Parameters</h4>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6">
<div>
<label for="systemSize" class="block text-sm font-medium mb-2">System Size: (Tons)</label>
<input id="systemSize" placeholder="Tons" name="systemSize" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5" min="1.0" autofocus required>
</div>
<div>
<label for="climateZone" class="block text-sm font-medium mb-2">Climate Zone</label>
<select id="climateZone" name="climateZone" class="w-full rounded-md border px-4 py-2 min-h-11">
<option value="hotHumid">Hot Humid (1A, 2A)</option>
<option value="moist">Moist (3A, 4A, 5A, 6A, 7A)</option>
<option value="dry">Dry (2B, 3B, 4B, 5B, 6B, 7B)</option>
<option value="marine">Marine (3C, 4C)</option>
</select>
</div>
<div>
<label for="filterType" class="block text-sm font-medium mb-2">Filter Type</label>
<select id="filterType" name="filterType" class="w-full rounded-md border px-4 py-2 min-h-11">
<option value="fiberglass">Fiberglass (MERV 1-4)</option>
<option value="pleatedBasic">Basic Pleated (MERV 5-8)</option>
<option value="pleatedBetter">Better Pleated (MERV 9-12)</option>
<option value="pleatedBest">Best Pleated (MERV 13-16)</option>
</select>
</div>
</div>
</div>
<div class="mt-6 lg:mt-0">
<h4 class="text-lg font-bold mb-4">Filter Parameters</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-4 space-y-6">
<div>
<label for="filterWidth" class="block text-sm font-medium mb-2">Width: (in)</label>
<input id="filterWidth" placeholder="Width" name="filterWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="filterHeight" class="block text-sm font-medium mb-2">Height: (in)</label>
<input id="filterHeight" placeholder="Height" name="filterHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
</div>
</div>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Pressure Drop</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,93 @@
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g clip-path="url(#clip0_429_11128)">
<path d="M20 3.99994H4L9.6 11.4666C9.85964 11.8128 10 12.2339 10 12.6666V19.9999L14 17.9999V12.6666C14 12.2339 14.1404 11.8128 14.4 11.4666L20 3.99994Z" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"></path>
</g>
<defs>
<clipPath id="clip0_429_11128"> <rect width="24" height="24" fill="white"></rect>
</clipPath>
</defs>
</g>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Filter Pressure Drop - Fan Law</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-s-lg" hx-target="#content" hx-push-url="true" hx-get="/filter-pressure-drop?mode=basic" type="button">Basic</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-e-lg" disabled type="button">Fan Law</button>
</div>
</div>
<form hx-post="/filter-pressure-drop" hx-target="#result">
<div class="space-y-6">
<div>
<div class="mt-6 lg:mt-0">
<h4 class="text-lg font-bold mb-4">Filter Parameters</h4>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6">
<div>
<label for="filterWidth" class="block text-sm font-medium mb-2">Width: (in)</label>
<input id="filterWidth" placeholder="Width" name="filterWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required autofocus>
</div>
<div>
<label for="filterHeight" class="block text-sm font-medium mb-2">Height: (in)</label>
<input id="filterHeight" placeholder="Height" name="filterHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="filterDepth" class="block text-sm font-medium mb-2">Depth: (in)</label>
<input id="filterDepth" placeholder="Depth" name="filterDepth" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="1.0" min="1.0" required>
</div>
</div>
</div>
<div class="mt-6 lg:mt-0">
<h4 class="text-lg font-bold mb-4">Airflow Parameters</h4>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6">
<div>
<label for="ratedAirflow" class="block text-sm font-medium mb-2">Rated Airflow: (CFM)</label>
<input id="ratedAirflow" placeholder="Rated or measured Airflow" name="ratedAirflow" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="ratedPressureDrop" class="block text-sm font-medium mb-2">Filter Pressure Drop: (in. w.c.)</label>
<input id="ratedPressureDrop" placeholder="Rated or measured pressure drop" name="ratedPressureDrop" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="designAirflow" class="block text-sm font-medium mb-2">Design Airflow: (CFM)</label>
<input id="designAirflow" placeholder="Design or target airflow" name="designAirflow" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="1.0" min="1.0" required>
</div>
</div>
</div>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Pressure Drop</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,42 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/filter-pressure-drop?mode=basic" hx-target="#content">Reset</button>
</div>
<div class=" grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
border border-blue-600 text-blue-600 bg-blue-100 p-6">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Filter Face Area</p>
<h3 class="text-3xl font-extrabold">3.47<span class="text-lg ms-2">ft²</span></h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Filter Face Velocity</p>
<h3 class="text-3xl font-extrabold">201.6<span class="text-lg ms-2">FPM</span></h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Initial Pressure Drop</p>
<h3 class="text-3xl font-extrabold">0.2"<span class="text-lg ms-2">w.c.</span></h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Maximum Allowable Pressure Drop</p>
<h3 class="text-3xl font-extrabold">0.15"<span class="text-lg ms-2">w.c.</span></h3>
</div>
</div>
<div id="warnings" class=" mt-6 p-4 rounded-lg shadow-lg
text-amber-500
bg-amber-100 dark:bg-amber-200
border border-amber-500">
<span class="font-semibold mb-4 border-b border-amber-500">Warning:</span>
<ul class="list-disc mx-10 mt-4">
<li>
Initial pressure drop is more than 50% of maximum allowable - consider using a larger filter or different
filter type with lower initial pressure drop.
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/filter-pressure-drop?mode=fanLaw" hx-target="#content">Reset</button>
</div>
<div class=" grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
border border-blue-600 text-blue-600 bg-blue-100 p-6">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Filter Face Velocity</p>
<h3 class="text-3xl font-extrabold">259.2<span class="text-lg ms-2">FPM</span></h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Predicted Pressure Drop</p>
<h3 class="text-3xl font-extrabold">0.13"<span class="text-lg ms-2">w.c.</span></h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Velocity Ratio</p>
<h3 class="text-3xl font-extrabold">1.12</h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Pressure Ratio</p>
<h3 class="text-3xl font-extrabold">1.27</h3>
</div>
</div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">
Predictions are based on fan laws where pressure drop varies with the square of the airflow ratio.
Results assume similar air properties and filter loading conditions.
</p>
</div>
</div>

View File

@@ -0,0 +1,102 @@
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
<path d="M7 21h10"/><path d="M12 3v18"/>
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Balance Point - Thermal</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-s-lg" disabled type="button">Thermal</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/balance-point?mode=economic&amp;heatLossMode=estimated" type="button">Economic</button>
</div>
</div>
<form hx-post="/balance-point" hx-target="#result">
<div class="space-y-6">
<div>
<label for="systemSize" class="block text-sm font-medium mb-2">System Size (Tons)</label>
<input id="systemSize" placeholder="System size" name="systemSize" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" autofocus required>
</div>
<div>
<div class="mb-4">
<h4 class="text-lg font-bold">Capacities</h4>
<p class="text-xs text-blue-500">Entering known capacities gives better results, otherwise capacities will be estimated.</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<label for="capacityAt47" class="block text-sm font-medium mb-2">Capacity @ 47° (BTU/h)</label>
<input id="capacityAt47" placeholder="Capacity @ 47° (optional)" name="capacityAt47" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5">
</div>
<div>
<label for="capacityAt17" class="block text-sm font-medium mb-2">Capacity @ 17° (BTU/h)</label>
<input id="capacityAt17" placeholder="Capacity @ 17° (optional)" name="capacityAt17" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5">
</div>
</div>
</div>
<div id="heatLossFields">
<div class="flex flex-wrap justify-between">
<h4 class="text-lg font-bold">Heat Loss - Estimated</h4>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-s-lg" disabled type="button">Estimated</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-e-lg" hx-get="/balance-point/heat-loss?mode=known" hx-target="#heatLossFields" type="button">Known</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div>
<label for="simplifiedHeatLoss" class="block text-sm font-medium mb-2">Building Size (ft²)</label>
<input id="simplifiedHeatLoss" placeholder="Square feet" name="simplifiedHeatLoss" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" required>
</div>
<div>
<label for="climateZone" class="block text-sm font-medium mb-2">Climate Zone</label>
<select id="climateZone" name="climateZone" class="w-full rounded-md border px-4 py-2 min-h-11">
<option value="CZ1">CZ1</option>
<option value="CZ2">CZ2</option>
<option value="CZ3">CZ3</option>
<option value="CZ4">CZ4</option>
<option value="CZ5">CZ5</option>
<option value="CZ6">CZ6</option>
<option value="CZ7">CZ7</option>
</select>
</div>
<div>
<label for="heatingDesignTemperature" class="block text-sm font-medium mb-2">Outdoor Design Temperature (°F)</label>
<input id="heatingDesignTemperature" placeholder="Design temperature (optional)" name="heatingDesignTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5">
</div>
</div>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Balance Point</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,41 @@
<div id="heatLossFields">
<div class="flex flex-wrap justify-between">
<h4 class="text-lg font-bold">Heat Loss - Estimated</h4>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-s-lg" disabled type="button">Estimated</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-e-lg" hx-get="/balance-point/heat-loss?mode=known" hx-target="#heatLossFields" type="button">Known</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div>
<label for="simplifiedHeatLoss" class="block text-sm font-medium mb-2">Building Size (ft²)</label>
<input id="simplifiedHeatLoss" placeholder="Square feet" name="simplifiedHeatLoss" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" required>
</div>
<div>
<label for="climateZone" class="block text-sm font-medium mb-2">Climate Zone</label>
<select id="climateZone" name="climateZone" class="w-full rounded-md border px-4 py-2 min-h-11">
<option value="CZ1">CZ1</option>
<option value="CZ2">CZ2</option>
<option value="CZ3">CZ3</option>
<option value="CZ4">CZ4</option>
<option value="CZ5">CZ5</option>
<option value="CZ6">CZ6</option>
<option value="CZ7">CZ7</option>
</select>
</div>
<div>
<label for="heatingDesignTemperature" class="block text-sm font-medium mb-2">Outdoor Design Temperature (°F)</label>
<input id="heatingDesignTemperature" placeholder="Design temperature (optional)" name="heatingDesignTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5">
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
<div id="heatLossFields">
<div class="flex flex-wrap justify-between">
<h4 class="text-lg font-bold">Heat Loss - Known</h4>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-s-lg" hx-get="/balance-point/heat-loss?mode=estimated" hx-target="#heatLossFields" type="button">Estimated</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-e-lg" disabled type="button">Known</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<label for="knownHeatLoss" class="block text-sm font-medium mb-2">Heat Loss (BTU/h)</label>
<input id="knownHeatLoss" placeholder="Heat loss" name="knownHeatLoss" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" required>
</div>
<div>
<label for="heatingDesignTemperature" class="block text-sm font-medium mb-2">Outdoor Design Temperature (°F)</label>
<input id="heatingDesignTemperature" placeholder="Design temperature" name="heatingDesignTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5" required>
</div>
</div>
</div>

View File

@@ -0,0 +1,51 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/balance-point?mode=economic" hx-target="#content">Reset</button>
</div>
<div class="w-full rounded-xl shadow-xl bg-blue-100 text-blue-600 border border-blue-600 py-4">
<div class="flex">
<div class="block text-blue-600 px-4">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
<path d="M7 21h10"/><path d="M12 3v18"/>
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>
</svg>
</div>
<p class="font-medium">Economic Balance Point</p>
</div>
<div>
<div class="grid grid-cols-1 justify-items-center mb-8">
<p class="font-medium">Balance Point</p>
<h3 class="text-3xl font-extrabold">-13<span class="text-lg ms-2">°F</span></h3>
</div>
<div class="grid grid-cols-2 space-y-6">
<div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Electric Cost</p>
<h3 class="text-3xl font-extrabold">$38.1<span class="text-lg ms-2">/ MMBTU</span></h3>
</div>
</div>
<div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Fuel Cost</p>
<h3 class="text-3xl font-extrabold">$29.56<span class="text-lg ms-2">/ MMBTU</span></h3>
</div>
</div>
</div>
<div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">COP at Balance Point</p>
<h3 class="text-3xl font-extrabold">1.29</h3>
</div>
</div>
</div>
</div>
<div id="warnings"></div>
</div>

View File

@@ -0,0 +1,57 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/balance-point?mode=thermal" hx-target="#content">Reset</button>
</div>
<div class="w-full rounded-xl shadow-xl bg-blue-100 text-blue-600 border border-blue-600 py-4">
<div class="flex">
<div class="block text-blue-600 px-4">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
<path d="M7 21h10"/><path d="M12 3v18"/>
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>
</svg>
</div>
<p class="font-medium">Thermal Balance Point</p>
</div>
<div>
<div class="grid grid-cols-1 justify-items-center mb-8">
<p class="font-medium">Balance Point</p>
<h3 class="text-3xl font-extrabold">36.9<span class="text-lg ms-2">°F</span></h3>
</div>
<div class="grid grid-cols-2 space-y-6">
<div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Heat Loss - Known</p>
<h3 class="text-3xl font-extrabold">45,667<span class="text-lg ms-2">BTU/h</span></h3>
</div>
</div>
<div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Heating Design Temperature</p>
<h3 class="text-3xl font-extrabold">5<span class="text-lg ms-2">°F</span></h3>
</div>
</div>
<div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Capacity @ 47°</p>
<h3 class="text-3xl font-extrabold">24,600<span class="text-lg ms-2">BTU/h</span></h3>
</div>
</div>
<div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Capacity @ 17°</p>
<h3 class="text-3xl font-extrabold">15,100<span class="text-lg ms-2">BTU/h</span></h3>
</div>
</div>
</div>
</div>
</div>
<div id="warnings"></div>
</div>

View File

@@ -0,0 +1,93 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/attic-ventilation" hx-target="#content">Reset</button>
</div>
<div class="space-y-6">
<div class=" w-full rounded-lg shadow-lg border-2 p-6 border-amber-500
text-amber-500 bg-amber-200">
<div class="flex text-2xl mb-6">
<div class="block text-amber-500">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path stroke="currentColor" d="M12 9C11.1077 8.98562 10.2363 9.27003 9.52424 9.808C8.81222 10.346 8.30055 11.1066 8.07061 11.9688C7.84068 12.8311 7.90568 13.7455 8.25529 14.5665C8.6049 15.3876 9.21904 16.0682 10 16.5M12 3V5M6.6 18.4L5.2 19.8M4 13H2M6.6 7.6L5.2 6.2M20 14.5351V4C20 2.89543 19.1046 2 18 2C16.8954 2 16 2.89543 16 4V14.5351C14.8044 15.2267 14 16.5194 14 18C14 20.2091 15.7909 22 18 22C20.2091 22 22 20.2091 22 18C22 16.5194 21.1956 15.2267 20 14.5351Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</svg>
</div>
<h4 class="font-extrabold ms-2">Ventilation Status: Inadequate</h4>
</div>
<div class="grid justify-items-stretch grid-cols-1 md:grid-cols-3">
<div class="justify-self-center">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Temperature Differential</p>
<h3 class="text-3xl font-extrabold">0 °F</h3>
</div>
</div>
<div class="justify-self-center">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Dewpoint Differential</p>
<h3 class="text-3xl font-extrabold">9 °F</h3>
</div>
</div>
<div class="justify-self-center">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Pressure Differential</p>
<h3 class="text-3xl font-extrabold">1 °F</h3>
</div>
</div>
</div>
</div>
<div class=" w-full rounded-lg border p-6 border-blue-600 text-blue-600 bg-blue-100 dark:bg-blue-300">
<div class="flex text-2xl mb-6">
<h4 class="font-extrabold ms-2">Required Ventilation</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 content-center">
<div class="justify-self-center">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Intake</p>
<h3 class="text-3xl font-extrabold">4.9 ft²</h3>
</div>
</div>
<div class="justify-self-center">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Exhaust</p>
<h3 class="text-3xl font-extrabold">3.3 ft²</h3>
</div>
</div>
</div>
<div class="mt-8">
<h4 class="font-bold">Recommendations:</h4>
<ul class="list-disc mx-10">
<li>Add 4.9 ft² of intake ventilation.</li>
<li>Add 3.3 ft² of exhaust ventilation.</li>
<li>Consider adding more exhaust ventilation to balance pressure.</li>
</ul>
</div>
</div>
<div id="warnings" class=" mt-6 p-4 rounded-lg shadow-lg
text-amber-500
bg-amber-100 dark:bg-amber-200
border border-amber-500">
<span class="font-semibold mb-4 border-b border-amber-500">Warning:</span>
<ul class="list-disc mx-10 mt-4">
<li>High moisture levels in the attic - increased ventilation is recommended</li>
</ul>
</div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">
Calculations are based on standard ventilation guidelines and building codes.
Local codes may vary. Consider consulting with a qualified professional for specific
recommendations.
</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,82 @@
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path stroke="currentColor" d="M12 9C11.1077 8.98562 10.2363 9.27003 9.52424 9.808C8.81222 10.346 8.30055 11.1066 8.07061 11.9688C7.84068 12.8311 7.90568 13.7455 8.25529 14.5665C8.6049 15.3876 9.21904 16.0682 10 16.5M12 3V5M6.6 18.4L5.2 19.8M4 13H2M6.6 7.6L5.2 6.2M20 14.5351V4C20 2.89543 19.1046 2 18 2C16.8954 2 16 2.89543 16 4V14.5351C14.8044 15.2267 14 16.5194 14 18C14 20.2091 15.7909 22 18 22C20.2091 22 22 20.2091 22 18C22 16.5194 21.1956 15.2267 20 14.5351Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</svg>
</div>
<h2 class="text-2xl font-extrabold">HVAC System Performance</h2>
</div>
<form hx-post="/hvac-system-performance" hx-target="#result">
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="systemSize" class="block text-sm font-medium mb-2">System Size (Tons)</label>
<input id="systemSize" placeholder="System size" name="systemSize" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" autofocus required>
</div>
<div>
<label for="airflow" class="block text-sm font-medium mb-2">Airflow (CFM)</label>
<input id="airflow" placeholder="Airflow" name="airflow" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" required>
</div>
<div>
<label for="altitude" class="block text-sm font-medium mb-2">Altitude (ft.)</label>
<input id="altitude" placeholder="Project altitude (Optional)" name="altitude" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-4">
<h3 class="text-lg font-medium">Return Air</h3>
<div>
<label for="returnAirTemperature" class="block text-sm font-medium mb-2">Dry Bulb (°F)</label>
<input id="returnAirTemperature" placeholder="Return temperature" name="returnAirTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500">
</div>
<div>
<label for="returnAirHumidity" class="block text-sm font-medium mb-2">Indoor Humdity (%)</label>
<input id="returnAirHumidity" placeholder="Return humidity" name="returnAirHumidity" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
</div>
<div class="space-y-4">
<h3 class="text-lg font-medium">Supply Air</h3>
<div>
<label for="supplyAirTemperature" class="block text-sm font-medium mb-2">Dry Bulb (°F)</label>
<input id="supplyAirTemperature" placeholder="Supply temperature" name="supplyAirTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500">
</div>
<div>
<label for="supplyAirHumidity" class="block text-sm font-medium mb-2">Indoor Humdity (%)</label>
<input id="supplyAirHumidity" placeholder="Supply humidity" name="supplyAirHumidity" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
</div>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Performance</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,95 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/hvac-system-performance" hx-target="#content">Reset</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="rounded-xl text-blue-600 bg-blue-100 border border-blue-600 p-6">
<h4 class="text-lg font-semibold mb-2">Total Capacity</h4>
<div class="text-3xl font-bold">2.9<span class="text-xl ps-2">Tons</span></div>
<div class="text-sm mt-1">35,396.9 BTU/h</div>
</div>
<div class="rounded-xl text-green-600 bg-green-100 border border-green-600 p-6">
<h4 class="text-lg font-semibold mb-2">Sensible Capacity</h4>
<div class="text-3xl font-bold">1.4<span class="text-xl ps-2">Tons</span></div>
<div class="text-sm mt-1">17,280 BTU/h</div>
</div>
<div class="rounded-xl text-purple-600 bg-purple-100 border border-purple-600 p-6">
<h4 class="text-lg font-semibold mb-2">Latent Capacity</h4>
<div class="text-3xl font-bold">1.5<span class="text-xl ps-2">Tons</span></div>
<div class="text-sm mt-1">18,116.9 BTU/h</div>
</div>
</div>
<div class="mt-8 border rounded-xl shadow-lg">
<h4 class="text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300">System Performance Metrics</h4>
<div class="flex justify-between items-center p-4">
<div class="space-y-3">
<div class="mb-8">
<span class="text-sm">Airflow per Ton</span>
<p class="text-2xl font-semibold">400<span class="text-base font-normal ps-2">CFM/ton</span></p>
</div>
<div>
<div>
<span class="text-sm">Temperature Split</span>
<p class="text-2xl font-semibold">20<span class="text-base font-normal ps-2">°F</span></p>
</div>
<p class="text-xs">Target: 20.83 °F</p>
</div>
</div>
<div class="space-y-3">
<div class="mb-8">
<span class="text-sm">Moisture Removal</span>
<p class="text-2xl font-semibold">2<span class="text-base font-normal ps-2">gal/h</span></p>
</div>
<div>
<span class="text-sm">Sensible Heat Ratio</span>
<p class="text-2xl font-semibold">0.5<span class="text-base font-normal ps-2">%</span></p>
</div>
</div>
</div>
<div id="warnings" class=" mt-6 p-4 rounded-lg shadow-lg
text-amber-500
bg-amber-100 dark:bg-amber-200
border border-amber-500 mb-4 mx-8">
<span class="font-semibold mb-4 border-b border-amber-500">Warning:</span>
<ul class="list-disc mx-10 mt-4">
<li>Low sensible heat ratio may indicate excessive dehumidification or low airflow.</li>
</ul>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
<div class="rounded-xl border border-blue-600 dark:border-gray-400 bg-blue-100 dark:bg-slate-700 p-4">
<h4 class=" text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300">Return Air Properties</h4>
<div class="w-full grid grid-cols-1 gap-4">
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Dew Point: </span><span class="font-light">64.3 °F</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Wet Bulb: </span><span class="font-light">67.64 °F</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Enthalpy: </span><span class="font-light">32.75 Btu/lb</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Density: </span><span class="font-light">0.07 lb/ft³</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Vapor Pressure: </span><span class="font-light">0.3 psi</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Specific Volume: </span><span class="font-light">14.2</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Absolute Humidity: </span><span class="font-light">94.18 gr/ft³</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Humidity Ratio: </span><span class="font-light">0.01</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Degree of Saturation: </span><span class="font-light">0.66</span></div>
</div>
</div>
<div class="rounded-xl border border-blue-600 dark:border-gray-400 bg-blue-100 dark:bg-slate-700 p-4">
<h4 class=" text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300">Supply Air Properties</h4>
<div class="w-full grid grid-cols-1 gap-4">
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Dew Point: </span><span class="font-light">52.87 °F</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Wet Bulb: </span><span class="font-light">54.01 °F</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Enthalpy: </span><span class="font-light">22.92 Btu/lb</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Density: </span><span class="font-light">0.07 lb/ft³</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Vapor Pressure: </span><span class="font-light">0.2 psi</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Specific Volume: </span><span class="font-light">13.57</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Absolute Humidity: </span><span class="font-light">61.47 gr/ft³</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Humidity Ratio: </span><span class="font-light">0.01</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Degree of Saturation: </span><span class="font-light">0.89</span></div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,35 @@
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-8 h-8">
<path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"></path>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Mold Risk Calculator</h2>
</div>
<form hx-post="/mold-risk" hx-target="#result">
<div class="space-y-6">
<div>
<label for="temperature" class="block text-sm font-medium mb-2">Indoor Temperature (°F)</label>
<input id="temperature" placeholder="Dry bulb temperature" name="temperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
</div>
<div>
<label for="humidity" class="block text-sm font-medium mb-2">Indoor Humdity (%)</label>
<input id="humidity" placeholder="Relative humidity" name="humidity" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Mold Risk</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,60 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/mold-risk" hx-target="#content">Reset</button>
</div>
<div class="p-2 rounded-lg shadow-lg bg-amber-200 border-2 border border-amber-500">
<div class="text-amber-500">
<div class="flex flex-wrap mt-2">
<div class="w-full sm:w-1/2 flex gap-2">
<div class="block text-amber-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6">
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path>
<path d="M12 9v4"></path>
<path d="M12 17h.01"></path>
</svg>
</div>
Risk Level: Moderate<span class="text-lg font-extrabold"></span>
</div>
<div class="w-full sm:w-1/2 gap-2"><span class="font-semibold">Estimated Days to Mold Growth: </span><span>30 days</span></div>
</div>
<div class="mt-6 pb-4">
<p class="font-semibold mb-4">
<u>Recommendations:</u>
</p>
<ul class="list-disc mx-10">
<li>Improve ventilation to reduce moisture accumulation</li>
<li>Inspect for and repair any water leaks or intrusion</li>
</ul>
</div>
</div>
</div>
<div class="w-full rounded-lg border mt-8">
<h3 class="flex justify-center text-xl font-semibold mb-6 mt-2">Psychrometric Properties</h3>
<div class="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2 px-4 pb-4">
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Dew Point: </span><span class="font-light">64.3 °F</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Wet Bulb: </span><span class="font-light">67.7 °F</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Enthalpy: </span><span class="font-light">32.33 Btu/lb</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Density: </span><span class="font-light">0.07 lb/ft³</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Vapor Pressure: </span><span class="font-light">0.3 psi</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Specific Volume: </span><span class="font-light">13.78</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Absolute Humidity: </span><span class="font-light">91.4 gr/ft³</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Humidity Ratio: </span><span class="font-light">0.01</span></div>
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Degree of Saturation: </span><span class="font-light">0.66</span></div>
</div>
</div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">
These calculations are based on typical indoor conditions and common mold species. Actual mold growth can
vary based on surface materials, air movement, and other environmental factors. Always address moisture
issues promptly and consult professionals for severe cases.
</p>
</div>
</div>

View File

@@ -0,0 +1,47 @@
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"></path>
<path d="M12.56 6.6A10.97 10.97 0 0014 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 01-11.91 4.97"></path>
</g>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Psychrometric Properties</h2>
</div>
<form hx-post="/psychrometric-properties" hx-target="#result">
<div class="space-y-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<label for="temperature" class="block text-sm font-medium mb-2">Temperature (°F)</label>
<input id="temperature" placeholder="Dry bulb temperature" name="temperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1" autofocus required>
</div>
<div>
<label for="humidity" class="block text-sm font-medium mb-2">Relative Humidity (%)</label>
<input id="humidity" placeholder="Relative humidity" name="humidity" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1" max="100" required>
</div>
</div>
<div>
<label for="altitude" class="block text-sm font-medium mb-2">Altitude (ft.)</label>
<input id="altitude" placeholder="Altitude (optional)" name="altitude" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1">
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Psychrometrics</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,58 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/psychrometric-properties" hx-target="#content">Reset</button>
</div>
<div class="w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6">
<div class="flex mb-8">
<div class="block text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"></path>
<path d="M12.56 6.6A10.97 10.97 0 0014 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 01-11.91 4.97"></path>
</g>
</svg>
</div>
<h3 class="text-xl px-2 font-semibold">Psychrometrics</h3>
</div>
<div class="grid grid-cols-3 gap-4">
<div class="rounded-lg bg-purple-200 border border-purple-600 text-purple-600">
<div class="grid grid-cols-1 justify-items-center my-8">
<p class="font-medium">Dew Point</p>
<h3 class="text-3xl font-extrabold">64.3<span class="text-lg ms-2">°F</span></h3>
</div>
</div>
<div class="rounded-lg bg-orange-200 border border-orange-600 text-orange-600">
<div class="grid grid-cols-1 justify-items-center my-8">
<p class="font-medium">Enthalpy</p>
<h3 class="text-3xl font-extrabold">32.8<span class="text-lg ms-2">Btu/lb</span></h3>
</div>
</div>
<div class="rounded-lg bg-green-200 border border-green-600 text-green-600">
<div class="grid grid-cols-1 justify-items-center my-8">
<p class="font-medium">Wet Bulb</p>
<h3 class="text-3xl font-extrabold">67.6<span class="text-lg ms-2">°F</span></h3>
</div>
</div>
</div>
<div class="mt-8">
<h4 class="text-lg font-semibold">Other Properties</h4>
<div class="rounded-lg border border-blue-300">
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Density: </span><span class="font-light">0.07 lb/ft³</span></div>
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Vapor Pressure: </span><span class="font-light">0.3 psi</span></div>
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Specific Volume: </span><span class="font-light">14.2</span></div>
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Absolute Humidity: </span><span class="font-light">94.18 gr/ft³</span></div>
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Humidity Ratio: </span><span class="font-light">0.01</span></div>
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Degree of Saturation: </span><span class="font-light">0.66</span></div>
</div>
</div>
</div>
<div id="warnings"></div>
</div>

View File

@@ -0,0 +1,80 @@
<div class="relative">
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left-right">
<path d="M8 3 4 7l4 4"/>
<path d="M4 7h16"/>
<path d="m16 21 4-4-4-4"/>
<path d="M20 17H4"/>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Room Pressure Calculator - Known Airflow</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-s-lg" disabled type="button">Known Airflow</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/room-pressure?mode=measuredPressure" type="button">Measured Pressure</button>
</div>
</div>
<form hx-post="/room-pressure" hx-target="#result" class="mt-6">
<div class="space-y-6">
<div>
<label for="targetRoomPressure" class="block text-sm font-medium mb-2">Target Room Pressure (Pascals)</label>
<input id="targetRoomPressure" placeholder="Room pressure (max 3 pa.)" name="targetRoomPressure" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" max="3.0" autofocus required>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<label for="doorWidth" class="block text-sm font-medium mb-2">Door Width (in.)</label>
<input id="doorWidth" placeholder="Width" name="doorWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="doorHeight" class="block text-sm font-medium mb-2">Door Height (in.)</label>
<input id="doorHeight" placeholder="Height" name="doorHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
</div>
<div>
<label for="doorUndercut" class="block text-sm font-medium mb-2">Door Undercut (in.)</label>
<input id="doorUndercut" placeholder="Undercut height" name="doorUndercut" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="supplyAirflow" class="block text-sm font-medium mb-2">Supply Airflow (CFM)</label>
<input id="supplyAirflow" placeholder="Airflow" name="supplyAirflow" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
Preferred Grille Height<label for="preferredGrilleHeight" class="block text-sm font-medium mb-2"></label>
<select id="preferredGrilleHeight" name="preferredGrilleHeight" class="w-full px-4 py-2 rounded-md border">
<option value="4">4"</option>
<option value="6">6"</option>
<option value="8">8"</option>
<option value="10">10"</option>
<option value="12">12"</option>
<option value="14">14"</option>
</select>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Return Path Size</button>
</div>
</div>
</form>
<div id="result"></div>
</div>

View File

@@ -0,0 +1,73 @@
<div class="relative">
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left-right">
<path d="M8 3 4 7l4 4"/>
<path d="M4 7h16"/>
<path d="m16 21 4-4-4-4"/>
<path d="M20 17H4"/>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Room Pressure Calculator - Measured Pressure</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-s-lg" hx-target="#content" hx-push-url="true" hx-get="/room-pressure?mode=knownAirflow" type="button">Known Airflow</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-e-lg" disabled type="button">Measured Pressure</button>
</div>
</div>
<form hx-post="/room-pressure" hx-target="#result" class="mt-6">
<div class="space-y-6">
<div>
<label for="measuredRoomPressure" class="block text-sm font-medium mb-2">Measured Room Pressure (Pascals)</label>
<input id="measuredRoomPressure" placeholder="Measured pressure" name="measuredRoomPressure" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<label for="doorWidth" class="block text-sm font-medium mb-2">Door Width (in.)</label>
<input id="doorWidth" placeholder="Width" name="doorWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="doorHeight" class="block text-sm font-medium mb-2">Door Height (in.)</label>
<input id="doorHeight" placeholder="Height" name="doorHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
</div>
<div>
<label for="doorUndercut" class="block text-sm font-medium mb-2">Door Undercut (in.)</label>
<input id="doorUndercut" placeholder="Undercut height" name="doorUndercut" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
Preferred Grille Height<label for="preferredGrilleHeight" class="block text-sm font-medium mb-2"></label>
<select id="preferredGrilleHeight" name="preferredGrilleHeight" class="w-full px-4 py-2 rounded-md border">
<option value="4">4"</option>
<option value="6">6"</option>
<option value="8">8"</option>
<option value="10">10"</option>
<option value="12">12"</option>
<option value="14">14"</option>
</select>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Return Path Size</button>
</div>
</div>
</form>
<div id="result"></div>
</div>

View File

@@ -0,0 +1,34 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/room-pressure?mode=knownAirflow" hx-target="#content">Reset</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="rounded-xl p-6 bg-blue-100 border border-blue-600 text-blue-600">
<h4 class="text-xl font-bold">Return / Transfer Grille</h4>
<div class="flex justify-between mt-6"><span class="font-semibold">Standard Size:</span><span> 14" x 14"</span></div>
<div class="flex justify-between mt-3"><span class="font-semibold">Required Net Free Area:</span><span> 0.5 in<sup>2</sup></span></div>
<div class="mt-8 text-sm"><span class="font-semibold">Note: </span><span>Select a grille with at least 0.5 in<sup>2</sup><span> net free area.</span></span></div>
</div>
<div class="rounded-xl p-6 bg-purple-100 border border-purple-600 text-purple-600">
<h4 class="text-xl font-bold">Return / Transfer Duct</h4>
<div class="flex justify-between mt-6"><span class="font-semibold">Standard Size:</span><span>9"</span></div>
<div class="flex justify-between mt-3"><span class="font-semibold">Air Velocity:</span><span>452.7 FPM</span></div>
</div>
</div>
<div id="warnings"></div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">
Calculations are based on a target velocity of 400 FPM for return/transfer air paths.
The required net free area is the minimum needed - select a grille that meets or exceeds this value.
Verify manufacturer specifications for actual net free area of selected grilles.
</p>
</div>
</div>

View File

@@ -0,0 +1,34 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/room-pressure?mode=measuredPressure" hx-target="#content">Reset</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="rounded-xl p-6 bg-blue-100 border border-blue-600 text-blue-600">
<h4 class="text-xl font-bold">Return / Transfer Grille</h4>
<div class="flex justify-between mt-6"><span class="font-semibold">Standard Size:</span><span> 14" x 14"</span></div>
<div class="flex justify-between mt-3"><span class="font-semibold">Required Net Free Area:</span><span> 0.5 in<sup>2</sup></span></div>
<div class="mt-8 text-sm"><span class="font-semibold">Note: </span><span>Select a grille with at least 0.5 in<sup>2</sup><span> net free area.</span></span></div>
</div>
<div class="rounded-xl p-6 bg-purple-100 border border-purple-600 text-purple-600">
<h4 class="text-xl font-bold">Return / Transfer Duct</h4>
<div class="flex justify-between mt-6"><span class="font-semibold">Standard Size:</span><span>9"</span></div>
<div class="flex justify-between mt-3"><span class="font-semibold">Air Velocity:</span><span>440.7 FPM</span></div>
</div>
</div>
<div id="warnings"></div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">
Calculations are based on a target velocity of 400 FPM for return/transfer air paths.
The required net free area is the minimum needed - select a grille that meets or exceeds this value.
Verify manufacturer specifications for actual net free area of selected grilles.
</p>
</div>
</div>

View File

@@ -0,0 +1,52 @@
<div class="relative">
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Capacitor Calculator - Test Capacitor</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-s-lg" disabled type="button">Test Capacitor</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/capacitor-calculator?mode=size" type="button">Size Capacitor</button>
</div>
</div>
<form hx-post="/capacitor-calculator" hx-target="#result" class="mt-6">
<div class="space-y-6">
<div>
<label for="startWindingAmps" class="block text-sm font-medium mb-2">Start Winding: (amps)</label>
<input id="startWindingAmps" placeholder="Current amps" name="startWindingAmps" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
</div>
<div>
<label for="runToCommonVoltage" class="block text-sm font-medium mb-2">Run to Common: (volts)</label>
<input id="runToCommonVoltage" placeholder="Voltage" name="runToCommonVoltage" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="ratedCapacitorSize" class="block text-sm font-medium mb-2">Capacitor Rated Size: (µF)</label>
<input id="ratedCapacitorSize" placeholder="Size (optional)" name="ratedCapacitorSize" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1">
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Test Capacitor</button>
</div>
</div>
</form>
<div id="result"></div>
</div>

View File

@@ -0,0 +1,45 @@
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-gauge">
<path d="M15.6 2.7a10 10 0 1 0 5.7 5.7"/>
<circle cx="12" cy="12" r="2"/>
<path d="M13.4 10.6 19 5"/>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Hydronic System Pressure</h2>
</div>
<form hx-post="/hydronic-system-pressure" hx-target="#result">
<div class="space-y-6">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="height" class="block text-sm font-medium mb-2">Height (ft.)</label>
<input id="height" placeholder="Building height" name="height" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1" autofocus required>
</div>
<div>
<label for="waterTemperature" class="block text-sm font-medium mb-2">Water Temperature (°F)</label>
<input id="waterTemperature" placeholder="Temperature (optional)" name="waterTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="32.0" step="0.1">
</div>
</div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">
Water temperature should be the coldest water temperature the system sees, which for boilers will be
when the system is filled with water.
</p>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate System Pressure</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,29 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/hydronic-system-pressure" hx-target="#content">Reset</button>
</div>
<div class="w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6">
<div class="grid grid-cols-2">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Pressure</p>
<h3 class="text-3xl font-extrabold">10.22<span class="text-lg ms-2">psi</span></h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Water Density</p>
<h3 class="text-3xl font-extrabold">62.58<span class="text-lg ms-2">lb/ft³</span></h3>
</div>
</div>
</div>
<div id="warnings"></div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">Expansion tank pressure should match system fill pressure.</p>
</div>
</div>

View File

@@ -0,0 +1,68 @@
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-footprints">
<path d="M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z"/>
<path d="M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z"/>
<path d="M16 17h4"/>
<path d="M4 13h4"/>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Feet of Head</h2>
</div>
<form hx-post="/feet-of-head" hx-target="#result">
<div class="space-y-6">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="pressure" class="block text-sm font-medium mb-2">Pressure (psi)</label>
<input id="pressure" placeholder="Pressure" name="pressure" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1" autofocus required>
</div>
<div>
<label for="waterTemperature" class="block text-sm font-medium mb-2">Water Temperature (°F)</label>
<input id="waterTemperature" placeholder="Temperature (optional)" name="waterTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" min="32.0" step="0.1">
</div>
</div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">If water temperature is not supplied, then calculations will be based on 60°F water.</p>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Feet of Head</button>
</div>
</div>
</form>
<div id="result">
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/feet-of-head" hx-target="#content">Reset</button>
</div>
<div class="w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6">
<div class="grid grid-cols-2">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Feet of Head</p>
<h3 class="text-3xl font-extrabold">7.95<span class="text-lg ms-2">ft.</span></h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Water Density</p>
<h3 class="text-3xl font-extrabold">62.37<span class="text-lg ms-2">lb/ft³</span></h3>
</div>
</div>
</div>
<div id="warnings"></div>
</div>
</div>

View File

@@ -0,0 +1,24 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/feet-of-head" hx-target="#content">Reset</button>
</div>
<div class="w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6">
<div class="grid grid-cols-2">
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Feet of Head</p>
<h3 class="text-3xl font-extrabold">7.9<span class="text-lg ms-2">ft.</span></h3>
</div>
<div class="grid grid-cols-1 justify-items-center">
<p class="font-medium">Water Density</p>
<h3 class="text-3xl font-extrabold">62.58<span class="text-lg ms-2">lb/ft³</span></h3>
</div>
</div>
</div>
<div id="warnings"></div>
</div>

View File

@@ -0,0 +1,52 @@
<div class="relative">
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Capacitor Calculator - Size Capacitor</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-s-lg" hx-target="#content" hx-push-url="true" hx-get="/capacitor-calculator?mode=test" type="button">Test Capacitor</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-e-lg" disabled type="button">Size Capacitor</button>
</div>
</div>
<form hx-post="/capacitor-calculator" hx-target="#result" class="mt-6">
<div class="space-y-6">
<div>
<label for="runningAmps" class="block text-sm font-medium mb-2">Running Current: (amps)</label>
<input id="runningAmps" placeholder="Current amps" name="runningAmps" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
</div>
<div>
<label for="lineVoltage" class="block text-sm font-medium mb-2">Line Voltage</label>
<input id="lineVoltage" placeholder="Voltage" name="lineVoltage" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="powerFactor" class="block text-sm font-medium mb-2">Power Factor</label>
<input id="powerFactor" placeholder="Power factor (0-1)" name="powerFactor" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.01" min="0.1" max="1.00" required>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Size</button>
</div>
</div>
</form>
<div id="result"></div>
</div>

View File

@@ -0,0 +1,52 @@
<div class="relative">
<div class="flex flex-wrap justify-between">
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Capacitor Calculator - Test Capacitor</h2>
</div>
<div class="flex items-center gap-x-0 mb-6">
<button class=" font-bold py-2 px-4 transition-colors
bg-yellow-300 enabled:hover:bg-yellow-400
text-blue-600 rounded-s-lg" disabled type="button">Test Capacitor</button>
<button class=" font-bold py-2 px-4 transition-colors
bg-blue-500 enabled:hover:bg-blue-600
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/capacitor-calculator?mode=size" type="button">Size Capacitor</button>
</div>
</div>
<form hx-post="/capacitor-calculator" hx-target="#result" class="mt-6">
<div class="space-y-6">
<div>
<label for="startWindingAmps" class="block text-sm font-medium mb-2">Start Winding: (amps)</label>
<input id="startWindingAmps" placeholder="Current amps" name="startWindingAmps" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
</div>
<div>
<label for="runToCommonVoltage" class="block text-sm font-medium mb-2">Run to Common: (volts)</label>
<input id="runToCommonVoltage" placeholder="Voltage" name="runToCommonVoltage" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="ratedCapacitorSize" class="block text-sm font-medium mb-2">Capacitor Rated Size: (µF)</label>
<input id="ratedCapacitorSize" placeholder="Size (optional)" name="ratedCapacitorSize" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1">
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Test Capacitor</button>
</div>
</div>
</form>
<div id="result"></div>
</div>

View File

@@ -0,0 +1,32 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/capacitor-calculator?mode=size" hx-target="#content">Reset</button>
</div>
<div>
<div class=" w-full rounded-lg shadow-lg p-4
border-2 border-blue-600 bg-blue-100 text-blue-600">
<div class="flex justify-center"><span class="font-extrabold">Required Capacitor Size</span></div>
<div class="flex justify-between mb-2"><span class="font-semibold">Recommended Standard Size</span><span>60.0 µF</span></div>
<div class="flex justify-between mb-2"><span class="font-semibold">Calculated Size:</span><span>59.6 µF</span></div>
<div class="flex justify-between mb-2"><span class="font-semibold">Acceptable Range (±6%):</span><span>57.2 - 63.2 µF</span></div>
</div>
<div id="warnings" class=" mt-6 p-4 rounded-lg shadow-lg
text-amber-500
bg-amber-100 dark:bg-amber-200
border border-amber-500">
<span class="font-extrabold">Important Notes:</span>
<ul class="list-disc mx-10 mt-4">
<li>Always verify voltage rating matches the application.</li>
<li>Use the next larger size if exact match is unavailable.</li>
<li>Ensure capacitor is rated for continuous duty.</li>
<li>Consider ambient temperature in final selection.</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/capacitor-calculator?mode=test" hx-target="#content">Reset</button>
</div>
<div>
<div class="grid grid-cols-1 gap-4 mt-8">
<div class="bg-blue-100 rounded-lg border-2 border-blue-600 text-blue-500 px-4 pb-4">
<div class="flex justify-center mb-6 mt-2"><span class="text-2xl font-extrabold">Measured</span></div>
<div class="flex justify-between mb-2"><span class="font-semibold">Capacitance</span><span>33.26 µF</span></div>
<div class="flex justify-between mb-2"><span class="font-semibold">Acceptable Range (±6%):</span><span>31.9 - 35.3 µF</span></div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,45 @@
<div class="flex items-center gap-3 mb-6">
<div class="block text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"></path>
<path d="M12.56 6.6A10.97 10.97 0 0014 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 01-11.91 4.97"></path>
</g>
</svg>
</div>
<h2 class="text-2xl font-extrabold">Dehumidifier Sizing Calculator</h2>
</div>
<form hx-post="/dehumidifier-sizing" hx-target="#result">
<div class="space-y-6">
<div>
<label for="latentLoad" class="block text-sm font-medium mb-2">Latent Load (BTU/h)</label>
<input id="latentLoad" placeholder="Latent load from Manual-J" name="latentLoad" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
</div>
<div>
<label for="temperature" class="block text-sm font-medium mb-2">Indoor Temperature (°F)</label>
<input id="temperature" placeholder="Indoor dry bulb temperature" name="temperature" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<label for="humidity" class="block text-sm font-medium mb-2">Indoor Humdity (%)</label>
<input id="humidity" placeholder="Relative humidity" name="humidity" class=" w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
</div>
<div>
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
bg-yellow-300 dark:bg-blue-500
hover:bg-yellow-400 hover:dark:bg-blue-600
text-blue-500 dark:text-yellow-300">Calculate Dehumidifier Size</button>
</div>
</div>
</form>
<div id="result"></div>

View File

@@ -0,0 +1,59 @@
<div class=" mt-6 p-6 rounded-lg border border-blue-500
bg-blue-50 dark:bg-slate-600
text-blue-500 dark:text-slate-200">
<div class="relative">
<h3 class="text-xl font-semibold mb-4">Results</h3>
<button class=" font-bold px-4 py-2 rounded-md transition-colors
bg-blue-500 dark:bg-yellow-300
hover:bg-blue-600 hover:dark:bg-yellow-400
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/dehumidifier-sizing" hx-target="#content">Reset</button>
</div>
<div class="py-4 block">
<div class="mb-6 sm:grid sm:grid-cols-1 sm:gap-4 lg:flex items-center lg:justify-between
text-blue-500 dark:text-slate-200">
<div><span class="font-semibold">Base Moisture Load: </span><span class="font-light">78.4 pints/day</span></div>
</div>
<p class="font-semibold mb-4 dark:text-yellow-300">Recommended Size:</p><a target="_blank" href="https://www.santa-fe-products.com/product/ultra98-dehumidifier/" rel="noopener noreferrer">
<div class=" px-8 py-2 flex items-center justify-between border border-blue-700 rounded-lg shadow-lg group
dark:bg-blue-400 hover:bg-blue-300 hover:dark:bg-blue-600
transition-colors">
<div>
<span class="font-extrabold text-4xl text-blue-800">100 PPD</span>
<p class="text-sm mt-1">Click to view recommended model →</p>
</div>
<div class=" w-12 h-12 rounded-full flex items-center justify-center
bg-blue-500 dark:bg-blue-600
group-hover:bg-blue-600 group-hover:dark:bg-blue-700
transition-colors">
<div class="block text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"></path>
<path d="M12.56 6.6A10.97 10.97 0 0014 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 01-11.91 4.97"></path>
</g>
</svg>
</div>
</div>
</div></a>
</div>
<div id="warnings" class=" mt-6 p-4 rounded-lg shadow-lg
text-amber-500
bg-amber-100 dark:bg-amber-200
border border-amber-500">
<span class="font-semibold mb-4 border-b border-amber-500">Warning:</span>
<ul class="list-disc mx-10 mt-4">
<li>High relative humidity - unit may need to run continuously.</li>
</ul>
</div>
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm">
<p class="font-extrabold mb-3">Note:</p>
<p class="px-6">
Sizing is based on continuous operation at rated conditions. Actual performance will vary based on
operating conditions. Consider Energy Star rated units for better efficiency. For whole-house
applications, ensure proper air distribution and drainage.
</p>
</div>
</div>

View File

@@ -12,7 +12,7 @@ build-docker platform="linux/arm64":
run:
#!/usr/bin/env zsh
touch .build/browser-dev-sync
browser-sync start -p localhost:8080 --ws &
browser-sync start -p localhost:8080 --watch --files '.build/browser-dev-sync' &
watchexec -w Sources -e .swift -r 'swift build --product App && touch .build/browser-dev-sync' &
watchexec -w .build/browser-dev-sync --ignore-nothing -r '.build/debug/App serve --log debug'
@@ -27,3 +27,7 @@ push-image:
build-docker-production:
@docker build --platform "linux/amd64" -t {{docker_registiry}}/{{docker_image}}:{{docker_tag}} .
test-docker:
@docker build --tag {{docker_image}}:test --file Dockerfile.dev . \
&& docker run --interactive --rm {{docker_image}}:test swift test

View File

@@ -16,6 +16,7 @@
"tailwindcss": "^4.0.8"
},
"dependencies": {
"@tailwindcss/cli": "^4.0.8"
"@tailwindcss/cli": "^4.0.8",
"browser-sync": "^3.0.3"
}
}

1049
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff