merging the madness

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-17 20:48:15 +00:00
commit 9b9b9e2848
21 changed files with 20714 additions and 205 deletions

View file

@ -5,10 +5,11 @@
"remoteUser": "vscode",
"workspaceFolder": "/workspaces/assessment-model",
"postStartCommand": "bash .devcontainer/post-install.sh",
"forwardPorts": [3000],
"forwardPorts": [3000], # For vscode
"appPort": ["3000:3000"], # For devcontainer shell
"mounts": [
// Optional, just makes getting from Downloads (local env) easier
// "source=${localEnv:HOME},target=/workspaces/home,type=bind"
"source=${localEnv:HOME},target=/workspaces/home,type=bind"
],
"customizations": {
"vscode": {

102
devcontainer.sh Normal file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env bash
#
# devcontainer.sh — devcontainer helper for this repo
#
# Usage:
# ./devcontainer.sh <command>
#
# Commands:
# up build + start the devcontainer (idempotent)
# shell attach a bash shell; auto-ups if not running
# down stop the devcontainer
# rebuild remove + rebuild from scratch, no cache
#
# Examples:
# ./devcontainer.sh shell # one-shot: up if needed, then bash
# ./devcontainer.sh rebuild
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
REPO_ROOT="${SCRIPT_DIR}"
CONFIG_PATH="${REPO_ROOT}/.devcontainer/devcontainer.json"
VALID_COMMANDS=(up shell down rebuild)
# --- helpers ---------------------------------------------------------------
usage() {
sed -n '3,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit "${1:-0}"
}
die() {
echo "error: $*" >&2
exit 1
}
in_list() {
local needle="$1"
shift
local item
for item in "$@"; do
[[ "${item}" == "${needle}" ]] && return 0
done
return 1
}
container_id() {
# Find the running container for this repo via devcontainer labels.
docker ps -q \
--filter "label=devcontainer.local_folder=${REPO_ROOT}" \
--filter "label=devcontainer.config_file=${CONFIG_PATH}"
}
# --- argument parsing ------------------------------------------------------
[[ $# -eq 1 ]] || usage 1
COMMAND="$1"
in_list "${COMMAND}" "${VALID_COMMANDS[@]}" \
|| die "invalid command '${COMMAND}' (expected: ${VALID_COMMANDS[*]})"
[[ -f "${CONFIG_PATH}" ]] || die "config not found: ${CONFIG_PATH}"
DC_ARGS=(--workspace-folder "${REPO_ROOT}")
# --- dispatch --------------------------------------------------------------
case "${COMMAND}" in
up)
echo ">> bringing up devcontainer"
devcontainer up "${DC_ARGS[@]}"
;;
shell)
# Auto-up if not already running. `devcontainer up` is idempotent —
# it reuses an existing container, so this is cheap on warm starts.
if [[ -z "$(container_id)" ]]; then
echo ">> devcontainer not running, bringing it up first"
devcontainer up "${DC_ARGS[@]}"
fi
echo ">> attaching shell"
devcontainer exec "${DC_ARGS[@]}" bash 2>/dev/null \
|| devcontainer exec "${DC_ARGS[@]}" sh
;;
down)
cid="$(container_id)"
if [[ -z "${cid}" ]]; then
echo ">> devcontainer not running, nothing to stop"
exit 0
fi
echo ">> stopping devcontainer"
docker stop "${cid}"
;;
rebuild)
echo ">> rebuilding devcontainer from scratch"
devcontainer up "${DC_ARGS[@]}" --remove-existing-container --build-no-cache
;;
esac

View file

@ -29,7 +29,6 @@ export async function GET(req: Request) {
s3FileBucket: uploadedFiles.s3FileBucket,
s3UploadTimestamp: uploadedFiles.s3UploadTimestamp,
fileType: uploadedFiles.fileType,
source: uploadedFiles.source,
uprn: uploadedFiles.uprn,
landlordPropertyId: uploadedFiles.landlordPropertyId,
measureName: uploadedFiles.measureName,
@ -41,8 +40,7 @@ export async function GET(req: Request) {
id: String(row.id),
s3FileKey: row.s3FileKey,
s3FileBucket: row.s3FileBucket,
docType: row.fileType ?? null,
source: row.source ?? null,
docType: row.fileType ?? "unknown",
s3UploadTimestamp: row.s3UploadTimestamp.toISOString(),
uprn: row.uprn !== null ? String(row.uprn) : null,
landlordPropertyId: row.landlordPropertyId,

View file

@ -13,7 +13,7 @@ import {
import { cn } from "@/lib/utils";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useState } from "react";
// import BulkUploadComingSoonModal from "@/app/components/portfolio/BulkUploadComingSoonModal";
import BulkUploadComingSoonModal from "@/app/components/portfolio/BulkUploadComingSoonModal";
interface AddNewProps {
portfolioId: string;
@ -37,112 +37,112 @@ export default function AddNew({
return (
<>
{/* <BulkUploadComingSoonModal
<BulkUploadComingSoonModal
isOpen={isBulkUploadOpen}
onClose={() => setIsBulkUploadOpen(false)}
portfolioId={portfolioId}
/> */}
<Menu as="div" className="relative inline-block text-left">
<MenuButton
className="
/>
<Menu as="div" className="relative inline-block text-left">
<MenuButton
className="
inline-flex items-center gap-1 px-4 py-2 rounded-md
bg-gray-50 text-gray-900 hover:bg-midblue hover:text-gray-100
transition-colors text-sm font-medium
"
>
<DocumentPlusIcon className="h-4 w-4 mr-2" />
New Property
<ChevronDownIcon className="h-4 w-4 opacity-70" />
</MenuButton>
>
<DocumentPlusIcon className="h-4 w-4 mr-2" />
New Property
<ChevronDownIcon className="h-4 w-4 opacity-70" />
</MenuButton>
<MenuItems
className="
<MenuItems
className="
absolute right-0 mt-3 w-72 origin-top-right rounded-md
bg-white shadow-lg ring-1 ring-black/5 focus:outline-none
z-[9999] py-3
"
>
<div className="flex flex-col gap-2 px-3">
{/* Remote Assessment */}
<MenuItem>
{({ active }) => (
<button
onClick={handleRemoteAssessment}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100",
)}
>
<DocumentMagnifyingGlassIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
>
<div className="flex flex-col gap-2 px-3">
{/* Remote Assessment */}
<MenuItem>
{({ active }) => (
<button
onClick={handleRemoteAssessment}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100"
)}
>
<DocumentMagnifyingGlassIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900 flex items-center gap-2">
Remote Assessment
{loadingRemote && (
<span className="h-3 w-3 rounded-full border-2 border-gray-400 border-t-transparent animate-spin"></span>
)}
</span>
<span className="text-xs text-gray-500 leading-snug">
Run a remote assessment for a single property.
</span>
</div>
</button>
)}
</MenuItem>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900 flex items-center gap-2">
Remote Assessment
{loadingRemote && (
<span className="h-3 w-3 rounded-full border-2 border-gray-400 border-t-transparent animate-spin"></span>
)}
</span>
<span className="text-xs text-gray-500 leading-snug">
Run a remote assessment for a single property.
</span>
</div>
</button>
)}
</MenuItem>
{/* CSV Upload */}
<MenuItem>
{({ active }) => (
<button
onClick={() => setIsUploadCsvOpen(!isUploadCsvOpen)}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100",
)}
>
<TableCellsIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
{/* CSV Upload */}
<MenuItem>
{({ active }) => (
<button
onClick={() => setIsUploadCsvOpen(!isUploadCsvOpen)}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100"
)}
>
<TableCellsIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900">
File Import
</span>
<span className="text-xs text-gray-500 leading-snug">
For bulk uploads, please contact a Domna user.
</span>
</div>
</button>
)}
</MenuItem>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900">
File Import
</span>
<span className="text-xs text-gray-500 leading-snug">
For bulk uploads, please contact a Domna user.
</span>
</div>
</button>
)}
</MenuItem>
{/* Bulk Upload (Coming Soon) */}
<MenuItem>
{({ active }) => (
<button
onClick={() => setIsBulkUploadOpen(true)}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100",
)}
>
<RectangleStackIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
{/* Bulk Upload (Coming Soon) */}
<MenuItem>
{({ active }) => (
<button
onClick={() => setIsBulkUploadOpen(true)}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100"
)}
>
<RectangleStackIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900 flex items-center gap-2">
new: Bulk upload
<span className="text-[10px] font-semibold text-amber-700 bg-amber-100 px-1.5 py-0.5 rounded-full leading-none">
coming soon
</span>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900 flex items-center gap-2">
new: Bulk upload
<span className="text-[10px] font-semibold text-amber-700 bg-amber-100 px-1.5 py-0.5 rounded-full leading-none">
coming soon
</span>
<span className="text-xs text-gray-500 leading-snug">
Upload multiple addresses in one go.
</span>
</div>
</button>
)}
</MenuItem>
</div>
</MenuItems>
</Menu>
</span>
<span className="text-xs text-gray-500 leading-snug">
Upload multiple addresses in one go.
</span>
</div>
</button>
)}
</MenuItem>
</div>
</MenuItems>
</Menu>
</>
);
}

View file

@ -0,0 +1 @@
ALTER TABLE "bulk_address_uploads" ADD COLUMN "task_id" uuid;

View file

@ -0,0 +1 @@
ALTER TABLE "bulk_address_uploads" ADD COLUMN "task_id" uuid;

View file

@ -0,0 +1 @@
ALTER TABLE "bulk_address_uploads" ADD COLUMN "combined_output_s3_uri" text;

View file

@ -0,0 +1 @@
ALTER TABLE "bulk_address_uploads" ADD COLUMN "combined_output_s3_uri" text;

View file

@ -0,0 +1,27 @@
CREATE TABLE "property_removal_requests" (
"id" bigserial PRIMARY KEY NOT NULL,
"hubspot_deal_id" text NOT NULL,
"portfolio_id" bigint NOT NULL,
"reason" text NOT NULL,
"status" text DEFAULT 'pending' NOT NULL,
"requested_by" bigint NOT NULL,
"requested_at" timestamp with time zone DEFAULT now() NOT NULL,
"reviewed_by" bigint,
"reviewed_at" timestamp with time zone
);
--> statement-breakpoint
ALTER TABLE "installed_measure" ALTER COLUMN "measure_type" SET DATA TYPE text;--> statement-breakpoint
DROP TYPE "public"."measure_type";--> statement-breakpoint
CREATE TYPE "public"."measure_type" AS ENUM('air_source_heat_pump', 'boiler_upgrade', 'high_heat_retention_storage_heaters', 'secondary_heating', 'roomstat_programmer_trvs', 'time_temperature_zone_control', 'cylinder_thermostat', 'cavity_wall_insulation', 'extension_cavity_wall_insulation', 'external_wall_insulation', 'internal_wall_insulation', 'loft_insulation', 'flat_roof_insulation', 'room_roof_insulation', 'solid_floor_insulation', 'suspended_floor_insulation', 'double_glazing', 'secondary_glazing', 'draught_proofing', 'mechanical_ventilation', 'low_energy_lighting', 'solar_pv', 'hot_water_tank_insulation', 'sealing_open_fireplace');--> statement-breakpoint
ALTER TABLE "installed_measure" ALTER COLUMN "measure_type" SET DATA TYPE "public"."measure_type" USING "measure_type"::"public"."measure_type";--> statement-breakpoint
ALTER TABLE "uploaded_files" ALTER COLUMN "file_type" SET DATA TYPE text;--> statement-breakpoint
DROP TYPE "public"."file_type";--> statement-breakpoint
CREATE TYPE "public"."file_type" AS ENUM('photo_pack', 'site_note', 'rd_sap_site_note', 'pas_2023_ventilation', 'pas_2023_condition', 'pas_significance', 'par_photo_pack', 'pas_2023_property', 'pas_2023_occupancy', 'ecmk_site_note', 'ecmk_rd_sap_site_note', 'ecmk_survey_xml', 'pre_photo', 'mid_photo', 'post_photo', 'loft_hatch_photo', 'dmev_photos', 'door_undercut_photos', 'trickle_vent_photos', 'pre_installation_building_inspection', 'point_of_work_risk_assessment', 'claim_of_compliance', 'mcs_compliance_certificate', 'certificate_of_conformity', 'minor_works_electrical_certificate', 'trustmark_licence_numbers', 'operative_competency', 'ventilation_assessment_checklist', 'anemometer_readings', 'commissioning_records', 'part_f_ventilation_document', 'handover_pack', 'insurance_guarantee', 'workmanship_warranty', 'g98_notification', 'installer_qualifications', 'installer_feedback', 'contractor_other');--> statement-breakpoint
ALTER TABLE "uploaded_files" ALTER COLUMN "file_type" SET DATA TYPE "public"."file_type" USING "file_type"::"public"."file_type";--> statement-breakpoint
ALTER TABLE "property_removal_requests" ADD CONSTRAINT "property_removal_requests_portfolio_id_portfolio_id_fk" FOREIGN KEY ("portfolio_id") REFERENCES "public"."portfolio"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "property_removal_requests" ADD CONSTRAINT "property_removal_requests_requested_by_user_id_fk" FOREIGN KEY ("requested_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "property_removal_requests" ADD CONSTRAINT "property_removal_requests_reviewed_by_user_id_fk" FOREIGN KEY ("reviewed_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "idx_removal_requests_deal_id" ON "property_removal_requests" USING btree ("hubspot_deal_id");--> statement-breakpoint
CREATE INDEX "idx_removal_requests_portfolio_id" ON "property_removal_requests" USING btree ("portfolio_id");--> statement-breakpoint
ALTER TABLE "bulk_address_uploads" DROP COLUMN "task_id";--> statement-breakpoint
ALTER TABLE "bulk_address_uploads" DROP COLUMN "combined_output_s3_uri";

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1223,8 +1223,36 @@
{
"idx": 174,
"version": "7",
"when": 1776453858204,
"tag": "0174_condemned_lady_bullseye",
"when": 1776900000000,
"tag": "0174_bulk_upload_task_id",
"breakpoints": true
},
{
"idx": 175,
"version": "7",
"when": 1776434096854,
"tag": "0175_sweet_otto_octavius",
"breakpoints": true
},
{
"idx": 176,
"version": "7",
"when": 1776900120000,
"tag": "0176_bulk_upload_combined_output",
"breakpoints": true
},
{
"idx": 177,
"version": "7",
"when": 1776451871348,
"tag": "0177_wooden_dexter_bennett",
"breakpoints": true
},
{
"idx": 178,
"version": "7",
"when": 1776458454019,
"tag": "0178_parched_midnight",
"breakpoints": true
}
]

View file

@ -58,13 +58,6 @@ export const measureTypeEnum = pgEnum("measure_type", [
// Other fabric / hot water
"hot_water_tank_insulation",
"sealing_open_fireplace",
// Contractor workflow measures
"damp_mould",
"door_undercut",
"extractor_fan",
"loft_board",
"trickle_vent",
]);
export const recommendation = pgTable(

View file

@ -2,7 +2,6 @@ import { bigint, bigserial, pgEnum, pgTable, text, timestamp } from "drizzle-orm
import { user } from "./users";
export const fileType = pgEnum("file_type", [
// Survey documents (existing)
"photo_pack",
"site_note",
"rd_sap_site_note",

View file

@ -27,13 +27,15 @@ async function getPortfolioUsers(portfolioId: string): Promise<Collaborator[]> {
const users = Array.isArray(json) ? json : json.users; // support both shapes
// Guard + shape to Collaborator[]
return Array.isArray(users)
? users.map((u: any) => ({
portfolioUserId: String(u.portfolioUserId),
userId: String(u.userId),
name: u.name ?? null,
email: u.email ?? "",
role: u.role,
}))
? users
.filter((u: any) => u.role !== "creator") // 👈 filter out creator
.map((u: any) => ({
portfolioUserId: String(u.portfolioUserId),
userId: String(u.userId),
name: u.name ?? null,
email: u.email ?? "",
role: u.role,
}))
: [];
}
@ -249,20 +251,12 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
<TableCell>{c.name || "—"}</TableCell>
<TableCell>{c.email}</TableCell>
<TableCell className="min-w-40">
{c.role === "creator" || c.role === "admin" ? (
<span className="text-xs font-medium text-gray-500 px-2 py-1 bg-gray-100 rounded-md capitalize">
{c.role}
</span>
) : (
<RoleDropdown value={c.role as "read" | "write"} onChange={(r) => onChangeRole(c.portfolioUserId, r)} />
)}
<RoleDropdown value={c.role} onChange={(r) => onChangeRole(c.portfolioUserId, r)} />
</TableCell>
<TableCell className="text-right">
{c.role !== "creator" && (
<Button variant="destructive" className="bg-red-700" onClick={() => onRemove(c.portfolioUserId)}>
Remove
</Button>
)}
<Button variant="destructive" className="bg-red-700" onClick={() => onRemove(c.portfolioUserId)}>
Remove
</Button>
</TableCell>
</TableRow>
))

View file

@ -13,10 +13,10 @@ export type Role = typeof ROLE_OPTIONS[number];
export type Collaborator = {
portfolioUserId: string;
userId: string;
userId: string;
name?: string | null;
email: string;
role: Role | "creator" | "admin";
role: Role;
};
// Small role dropdown using shadcn Select

View file

@ -1,5 +1,4 @@
import { UsersPermissionsCard } from "../UsersPermissionsCard";
import { CapabilitiesCard } from "../CapabilitiesCard";
export default async function UserAccessPage(props: {
params: Promise<{ slug: string }>;
@ -9,7 +8,6 @@ export default async function UserAccessPage(props: {
return (
<div>
<UsersPermissionsCard portfolioId={slug} />
<CapabilitiesCard portfolioId={slug} />
</div>
);
}

View file

@ -217,8 +217,6 @@ export default function LiveTracker({
data={currentProject?.allDeals ?? []}
onOpenDrawer={handleOpenDrawer}
docStatusMap={docStatusMap}
portfolioId={portfolioId}
userCapability={userCapability}
/>
</div>
</TabsContent>
@ -360,8 +358,8 @@ export default function LiveTracker({
{/* ── Property detail drawer ─────────────────────────────────────── */}
<PropertyDetailDrawer
deal={detailDeal}
portfolioId={portfolioId}
onClose={() => setDetailDeal(null)}
portfolioId={portfolioId}
userRole={userRole}
userCapability={userCapability}
userEmail={userEmail}

View file

@ -260,81 +260,24 @@ function RemovalRequestSection({
}
// -----------------------------------------------------------------------
// Approval log types + helpers
// Approval log placeholder (expand into a real implementation as needed)
// -----------------------------------------------------------------------
type AuditEvent = {
id: string;
measureName: string;
action: string; // 'approved' | 'unapproved'
actedByEmail: string;
actedByName: string | null;
actedAt: string;
};
function formatDateTime(iso: string) {
return new Date(iso).toLocaleString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
function ApprovalLogSection({ dealId, portfolioId }: { dealId: string; portfolioId: string }) {
void dealId; void portfolioId;
return <p className="text-xs text-gray-400">No approvals recorded.</p>;
}
function ApprovalLogSection({
dealId,
portfolioId,
}: {
dealId: string;
portfolioId: string;
}) {
const { data, isLoading } = useQuery<{ events: AuditEvent[] }>({
queryKey: ["approvalEvents", portfolioId, dealId],
queryFn: async () => {
const res = await fetch(
`/api/portfolio/${portfolioId}/approvals?dealIds=${dealId}&include=events`,
);
if (!res.ok) throw new Error("Failed to fetch events");
return res.json();
},
staleTime: 30_000,
});
if (isLoading) {
return <p className="text-xs text-gray-400 py-2">Loading activity</p>;
}
const events = data?.events ?? [];
if (events.length === 0) {
return (
<p className="text-xs text-gray-400 py-2">No approval activity yet.</p>
);
}
return (
<div className="space-y-2 pt-1">
{events.map((e) => (
<div key={e.id} className="flex items-start gap-2 text-xs">
<span
className={`mt-0.5 shrink-0 px-1.5 py-0.5 rounded text-xs font-medium ${
e.action === "approved"
? "bg-emerald-50 text-emerald-700"
: "bg-red-50 text-red-600"
}`}
>
{e.action === "approved" ? "Approved" : "Unapproved"}
</span>
<div className="min-w-0">
<span className="font-medium text-gray-700">{e.measureName}</span>
<div className="text-gray-400 mt-0.5">
{e.actedByName ?? e.actedByEmail} · {formatDateTime(e.actedAt)}
</div>
</div>
</div>
))}
</div>
);
function formatDateTime(d: string | Date | null | undefined): string {
if (!d) return "";
try {
return new Date(d).toLocaleString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
} catch { return ""; }
}
// -----------------------------------------------------------------------
@ -465,8 +408,8 @@ function MilestoneTimeline({ deal }: { deal: ClassifiedDeal }) {
// -----------------------------------------------------------------------
interface PropertyDetailDrawerProps {
deal: ClassifiedDeal | null;
portfolioId: string;
onClose: () => void;
portfolioId: string;
userRole: string;
userCapability: PortfolioCapabilityType;
userEmail: string;
@ -483,7 +426,7 @@ export default function PropertyDetailDrawer({
const [isLogOpen, setIsLogOpen] = useState(false);
return (
<Drawer open={open} onOpenChange={(v) => { if (!v) { setIsLogOpen(false); onClose(); } }} direction="right">
<Drawer open={open} onOpenChange={(v) => !v && onClose()} direction="right">
<DrawerContent className="fixed right-0 top-0 bottom-0 h-full w-[42vw] min-w-80 max-w-lg rounded-l-2xl rounded-r-none mt-0 flex flex-col border-l border-t-0 border-b-0 border-r-0 border-brandblue/10 bg-white shadow-2xl overflow-hidden">
<div className="hidden" />

View file

@ -1,7 +1,7 @@
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { redirect } from "next/navigation";
import { and, eq, inArray } from "drizzle-orm";
import { eq, inArray, and } from "drizzle-orm";
import LiveTracker from "./LiveTracker";
import { computeLiveTrackerData } from "./transforms";
import { db } from "@/app/db/db";