feat: Adds snapshot tests and updates Dockerfile.dev for docker tests.

This commit is contained in:
2025-03-07 11:16:57 -05:00
parent 35a54db9a3
commit f34e0a8b25
18 changed files with 951 additions and 68 deletions

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

@@ -3,6 +3,7 @@ import Elementary
import HTMLSnapshotTesting
import Logging
import OrderedCollections
import PsychrometricClientLive
import Routes
import SnapshotTesting
import Testing
@@ -14,11 +15,13 @@ 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
@@ -33,15 +36,18 @@ struct ViewControllerTests {
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)),
@@ -60,7 +66,66 @@ struct ViewControllerTests {
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
))))
])
for route in arguments {
@@ -71,6 +136,8 @@ struct ViewControllerTests {
}
}
// swiftlint:enable function_body_length
}
private extension ViewController {

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,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

@@ -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