dosewise-home 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dosewise Home
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: dosewise-home
3
+ Version: 0.1.0
4
+ Summary: Local-first household medication schedule and refill planner
5
+ Author: Dosewise Home
6
+ License-Expression: MIT
7
+ Keywords: medication,refill,health,household,planner
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # Dosewise Home
18
+
19
+ Dosewise Home is a local-first household medication schedule and refill planner. Paste a simple medication cabinet CSV, then get a same-week refill queue, daily dose timeline, and pharmacist conversation prompts without creating an account or uploading private health notes.
20
+
21
+ ![Dosewise Home dashboard](assets/screenshots/dosewise-home.svg)
22
+
23
+ ## Features
24
+
25
+ - Runs locally from a small Python server and browser interface.
26
+ - Imports comma, tab, semicolon, or pipe-delimited medication rows.
27
+ - Estimates days left and prioritizes critical, soon, scheduled, and steady refills.
28
+ - Builds a daily dose timeline from common instruction words like morning, dinner, bedtime, and night.
29
+ - Flags plain-language review prompts for common household issues such as grapefruit cautions, duplicate acetaminophen, blood thinner review, and stacked sleep aids.
30
+ - Copies a concise refill brief for a caregiver, pharmacy call, or household planning note.
31
+
32
+ Dosewise Home organizes information only. It does not diagnose, prescribe, or replace medical advice from a clinician or pharmacist.
33
+
34
+ ## Installation
35
+
36
+ Python 3.10 or newer is required.
37
+
38
+ ```bash
39
+ git clone https://github.com/iwaheedsattar/dosewise-home.git
40
+ cd dosewise-home
41
+ python3 -m venv .venv
42
+ source .venv/bin/activate
43
+ python -m pip install -e .
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ Start the local app:
49
+
50
+ ```bash
51
+ dosewise-home
52
+ ```
53
+
54
+ Open [http://127.0.0.1:8765](http://127.0.0.1:8765), paste medication rows, and select **Build plan**.
55
+
56
+ Example CSV:
57
+
58
+ ```csv
59
+ name,owner,strength,instructions,quantity_left,doses_per_day,refill_date,notes
60
+ Atorvastatin,Sam,20mg,Take one tablet with dinner,12,1,2026-07-03,Avoid grapefruit
61
+ Lisinopril,Sam,10mg,Take every morning,24,1,2026-07-20,Blood pressure
62
+ Acetaminophen Cold,Lee,500mg,Take at bedtime if needed,4,2,2026-06-30,Cold medicine
63
+ ```
64
+
65
+ A fuller sample is available at `examples/household-medications.csv`.
66
+
67
+ ## CSV Columns
68
+
69
+ Required columns:
70
+
71
+ - `name`
72
+ - `owner`
73
+ - `strength`
74
+ - `instructions`
75
+ - `quantity_left`
76
+ - `doses_per_day`
77
+
78
+ Optional columns:
79
+
80
+ - `refill_date` in `YYYY-MM-DD`, `MM/DD/YYYY`, or `MM/DD/YY` format
81
+ - `notes`
82
+
83
+ ## Development
84
+
85
+ Run tests:
86
+
87
+ ```bash
88
+ python3 -m pytest
89
+ ```
90
+
91
+ Run without installing:
92
+
93
+ ```bash
94
+ python3 -m dosewise_home.server
95
+ ```
96
+
97
+ ## Roadmap
98
+
99
+ - Add printable weekly pill-box sheets.
100
+ - Add encrypted local browser storage for recurring household lists.
101
+ - Add optional OCR import from prescription labels.
102
+ - Add iCalendar reminders for refill windows.
103
+ - Package as a signed desktop application.
@@ -0,0 +1,87 @@
1
+ # Dosewise Home
2
+
3
+ Dosewise Home is a local-first household medication schedule and refill planner. Paste a simple medication cabinet CSV, then get a same-week refill queue, daily dose timeline, and pharmacist conversation prompts without creating an account or uploading private health notes.
4
+
5
+ ![Dosewise Home dashboard](assets/screenshots/dosewise-home.svg)
6
+
7
+ ## Features
8
+
9
+ - Runs locally from a small Python server and browser interface.
10
+ - Imports comma, tab, semicolon, or pipe-delimited medication rows.
11
+ - Estimates days left and prioritizes critical, soon, scheduled, and steady refills.
12
+ - Builds a daily dose timeline from common instruction words like morning, dinner, bedtime, and night.
13
+ - Flags plain-language review prompts for common household issues such as grapefruit cautions, duplicate acetaminophen, blood thinner review, and stacked sleep aids.
14
+ - Copies a concise refill brief for a caregiver, pharmacy call, or household planning note.
15
+
16
+ Dosewise Home organizes information only. It does not diagnose, prescribe, or replace medical advice from a clinician or pharmacist.
17
+
18
+ ## Installation
19
+
20
+ Python 3.10 or newer is required.
21
+
22
+ ```bash
23
+ git clone https://github.com/iwaheedsattar/dosewise-home.git
24
+ cd dosewise-home
25
+ python3 -m venv .venv
26
+ source .venv/bin/activate
27
+ python -m pip install -e .
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ Start the local app:
33
+
34
+ ```bash
35
+ dosewise-home
36
+ ```
37
+
38
+ Open [http://127.0.0.1:8765](http://127.0.0.1:8765), paste medication rows, and select **Build plan**.
39
+
40
+ Example CSV:
41
+
42
+ ```csv
43
+ name,owner,strength,instructions,quantity_left,doses_per_day,refill_date,notes
44
+ Atorvastatin,Sam,20mg,Take one tablet with dinner,12,1,2026-07-03,Avoid grapefruit
45
+ Lisinopril,Sam,10mg,Take every morning,24,1,2026-07-20,Blood pressure
46
+ Acetaminophen Cold,Lee,500mg,Take at bedtime if needed,4,2,2026-06-30,Cold medicine
47
+ ```
48
+
49
+ A fuller sample is available at `examples/household-medications.csv`.
50
+
51
+ ## CSV Columns
52
+
53
+ Required columns:
54
+
55
+ - `name`
56
+ - `owner`
57
+ - `strength`
58
+ - `instructions`
59
+ - `quantity_left`
60
+ - `doses_per_day`
61
+
62
+ Optional columns:
63
+
64
+ - `refill_date` in `YYYY-MM-DD`, `MM/DD/YYYY`, or `MM/DD/YY` format
65
+ - `notes`
66
+
67
+ ## Development
68
+
69
+ Run tests:
70
+
71
+ ```bash
72
+ python3 -m pytest
73
+ ```
74
+
75
+ Run without installing:
76
+
77
+ ```bash
78
+ python3 -m dosewise_home.server
79
+ ```
80
+
81
+ ## Roadmap
82
+
83
+ - Add printable weekly pill-box sheets.
84
+ - Add encrypted local browser storage for recurring household lists.
85
+ - Add optional OCR import from prescription labels.
86
+ - Add iCalendar reminders for refill windows.
87
+ - Package as a signed desktop application.
@@ -0,0 +1,3 @@
1
+ """Dosewise Home local medication planning tools."""
2
+
3
+ __all__ = ["planner"]
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date, datetime, time, timedelta
5
+ import csv
6
+ import io
7
+ import math
8
+ import re
9
+ from typing import Iterable
10
+
11
+
12
+ DOSE_TIME_HINTS = {
13
+ "morning": time(8, 0),
14
+ "breakfast": time(8, 0),
15
+ "noon": time(12, 0),
16
+ "lunch": time(12, 30),
17
+ "afternoon": time(15, 0),
18
+ "evening": time(18, 0),
19
+ "dinner": time(18, 30),
20
+ "bedtime": time(21, 30),
21
+ "night": time(21, 30),
22
+ }
23
+
24
+ INTERACTION_RULES = [
25
+ {
26
+ "name": "Grapefruit caution",
27
+ "pattern": re.compile(r"\b(grapefruit|atorvastatin|simvastatin|lovastatin)\b", re.I),
28
+ "message": "Grapefruit can affect several common cholesterol medicines. Confirm food guidance with the prescriber or pharmacist.",
29
+ },
30
+ {
31
+ "name": "Duplicate acetaminophen",
32
+ "pattern": re.compile(r"\b(acetaminophen|paracetamol|tylenol|cold|flu)\b", re.I),
33
+ "message": "Cold and flu products may contain acetaminophen. Track total daily amount to avoid accidental duplication.",
34
+ },
35
+ {
36
+ "name": "Blood thinner reminder",
37
+ "pattern": re.compile(r"\b(warfarin|apixaban|rivaroxaban|dabigatran|aspirin|ibuprofen|naproxen)\b", re.I),
38
+ "message": "Blood thinners and anti-inflammatory pain relievers can need extra review before combining.",
39
+ },
40
+ {
41
+ "name": "Sedation stack",
42
+ "pattern": re.compile(r"\b(diphenhydramine|benadryl|zolpidem|melatonin|sleep|nighttime)\b", re.I),
43
+ "message": "Sleep aids and antihistamines can compound drowsiness. Use a shared household note before nighttime doses.",
44
+ },
45
+ ]
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Medication:
50
+ name: str
51
+ owner: str
52
+ strength: str
53
+ instructions: str
54
+ quantity_left: float
55
+ doses_per_day: float
56
+ refill_date: date | None
57
+ notes: str = ""
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class RefillStatus:
62
+ medication: Medication
63
+ days_left: float
64
+ runout_date: date | None
65
+ urgency: str
66
+ message: str
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class ScheduleItem:
71
+ at: time
72
+ owner: str
73
+ medicine: str
74
+ strength: str
75
+ instructions: str
76
+
77
+
78
+ def parse_date(value: str | None) -> date | None:
79
+ if not value:
80
+ return None
81
+ value = value.strip()
82
+ if not value:
83
+ return None
84
+ for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y"):
85
+ try:
86
+ return datetime.strptime(value, fmt).date()
87
+ except ValueError:
88
+ continue
89
+ raise ValueError(f"Unsupported date format: {value!r}")
90
+
91
+
92
+ def parse_medications(text: str) -> list[Medication]:
93
+ stream = io.StringIO(text.strip())
94
+ sample = stream.read(2048)
95
+ stream.seek(0)
96
+ dialect = csv.Sniffer().sniff(sample, delimiters=",\t;|") if sample else csv.excel
97
+ reader = csv.DictReader(stream, dialect=dialect)
98
+ required = {"name", "owner", "strength", "instructions", "quantity_left", "doses_per_day"}
99
+ missing = required - {field.strip() for field in (reader.fieldnames or [])}
100
+ if missing:
101
+ raise ValueError(f"Missing columns: {', '.join(sorted(missing))}")
102
+
103
+ rows: list[Medication] = []
104
+ for row in reader:
105
+ if not any(row.values()):
106
+ continue
107
+ rows.append(
108
+ Medication(
109
+ name=(row.get("name") or "").strip(),
110
+ owner=(row.get("owner") or "Household").strip(),
111
+ strength=(row.get("strength") or "").strip(),
112
+ instructions=(row.get("instructions") or "").strip(),
113
+ quantity_left=float((row.get("quantity_left") or "0").strip()),
114
+ doses_per_day=max(float((row.get("doses_per_day") or "0").strip()), 0),
115
+ refill_date=parse_date(row.get("refill_date")),
116
+ notes=(row.get("notes") or "").strip(),
117
+ )
118
+ )
119
+ return rows
120
+
121
+
122
+ def estimate_refills(medications: Iterable[Medication], today: date | None = None) -> list[RefillStatus]:
123
+ today = today or date.today()
124
+ statuses: list[RefillStatus] = []
125
+ for med in medications:
126
+ if med.doses_per_day <= 0:
127
+ days_left = math.inf
128
+ runout = None
129
+ else:
130
+ days_left = med.quantity_left / med.doses_per_day
131
+ runout = today + timedelta(days=max(math.floor(days_left), 0))
132
+
133
+ if days_left <= 3:
134
+ urgency = "critical"
135
+ message = "Call today or move to the front of the refill queue."
136
+ elif days_left <= 7:
137
+ urgency = "soon"
138
+ message = "Refill this week and confirm pickup or delivery timing."
139
+ elif med.refill_date and med.refill_date <= today + timedelta(days=7):
140
+ urgency = "scheduled"
141
+ message = "A refill date is approaching; verify the prescription is active."
142
+ else:
143
+ urgency = "steady"
144
+ message = "Supply looks stable for the next week."
145
+
146
+ statuses.append(RefillStatus(med, days_left, runout, urgency, message))
147
+ return sorted(statuses, key=lambda item: (item.days_left, item.medication.owner, item.medication.name))
148
+
149
+
150
+ def infer_schedule(medications: Iterable[Medication]) -> list[ScheduleItem]:
151
+ items: list[ScheduleItem] = []
152
+ for med in medications:
153
+ text = f"{med.instructions} {med.notes}".lower()
154
+ matched = [slot for key, slot in DOSE_TIME_HINTS.items() if key in text]
155
+ if not matched:
156
+ matched = default_slots(med.doses_per_day)
157
+ for at in sorted(set(matched)):
158
+ items.append(ScheduleItem(at, med.owner, med.name, med.strength, med.instructions))
159
+ return sorted(items, key=lambda item: (item.at, item.owner, item.medicine))
160
+
161
+
162
+ def default_slots(doses_per_day: float) -> list[time]:
163
+ if doses_per_day >= 3:
164
+ return [time(8, 0), time(14, 0), time(21, 0)]
165
+ if doses_per_day >= 2:
166
+ return [time(8, 0), time(20, 0)]
167
+ return [time(9, 0)]
168
+
169
+
170
+ def find_cautions(medications: Iterable[Medication]) -> list[dict[str, str]]:
171
+ haystack = "\n".join(
172
+ f"{med.name} {med.instructions} {med.notes}" for med in medications
173
+ )
174
+ cautions = []
175
+ for rule in INTERACTION_RULES:
176
+ if rule["pattern"].search(haystack):
177
+ cautions.append({"name": rule["name"], "message": rule["message"]})
178
+ return cautions
179
+
180
+
181
+ def build_plan(text: str, today: date | None = None) -> dict[str, object]:
182
+ medications = parse_medications(text)
183
+ refills = estimate_refills(medications, today=today)
184
+ schedule = infer_schedule(medications)
185
+ return {
186
+ "medications": [med.__dict__ | {"refill_date": med.refill_date.isoformat() if med.refill_date else ""} for med in medications],
187
+ "refills": [
188
+ {
189
+ "name": status.medication.name,
190
+ "owner": status.medication.owner,
191
+ "days_left": None if math.isinf(status.days_left) else round(status.days_left, 1),
192
+ "runout_date": status.runout_date.isoformat() if status.runout_date else "",
193
+ "urgency": status.urgency,
194
+ "message": status.message,
195
+ }
196
+ for status in refills
197
+ ],
198
+ "schedule": [
199
+ {
200
+ "time": item.at.strftime("%H:%M"),
201
+ "owner": item.owner,
202
+ "medicine": item.medicine,
203
+ "strength": item.strength,
204
+ "instructions": item.instructions,
205
+ }
206
+ for item in schedule
207
+ ],
208
+ "cautions": find_cautions(medications),
209
+ }
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
4
+ from pathlib import Path
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ from .planner import build_plan
10
+
11
+
12
+ WEB = Path(__file__).resolve().parent / "web"
13
+
14
+
15
+ class DosewiseHandler(SimpleHTTPRequestHandler):
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, directory=str(WEB), **kwargs)
18
+
19
+ def do_POST(self) -> None:
20
+ if self.path != "/api/plan":
21
+ self.send_error(404)
22
+ return
23
+ length = int(self.headers.get("Content-Length", "0"))
24
+ payload = json.loads(self.rfile.read(length) or b"{}")
25
+ try:
26
+ plan = build_plan(payload.get("medications", ""))
27
+ self.send_json(200, plan)
28
+ except Exception as exc:
29
+ self.send_json(400, {"error": str(exc)})
30
+
31
+ def send_json(self, status: int, payload: object) -> None:
32
+ body = json.dumps(payload, indent=2).encode("utf-8")
33
+ self.send_response(status)
34
+ self.send_header("Content-Type", "application/json")
35
+ self.send_header("Content-Length", str(len(body)))
36
+ self.end_headers()
37
+ self.wfile.write(body)
38
+
39
+
40
+ def main(argv: list[str] | None = None) -> int:
41
+ argv = argv or sys.argv[1:]
42
+ port = int(argv[0] if argv else os.environ.get("DOSEWISE_PORT", "8765"))
43
+ server = ThreadingHTTPServer(("127.0.0.1", port), DosewiseHandler)
44
+ print(f"Dosewise Home running at http://127.0.0.1:{port}")
45
+ server.serve_forever()
46
+ return 0
47
+
48
+
49
+ if __name__ == "__main__":
50
+ raise SystemExit(main())
@@ -0,0 +1,110 @@
1
+ const sample = `name,owner,strength,instructions,quantity_left,doses_per_day,refill_date,notes
2
+ Atorvastatin,Sam,20mg,Take one tablet with dinner,12,1,2026-07-03,Avoid grapefruit
3
+ Lisinopril,Sam,10mg,Take every morning,24,1,2026-07-20,Blood pressure
4
+ Acetaminophen Cold,Lee,500mg,Take at bedtime if needed,4,2,2026-06-30,Cold medicine
5
+ Vitamin D,Maya,1000 IU,Take with breakfast,45,1,2026-08-15,Weekly pill box
6
+ Melatonin,Lee,3mg,Take at night if needed,7,1,2026-07-05,Sleep aid`;
7
+
8
+ const medications = document.querySelector("#medications");
9
+ const planButton = document.querySelector("#plan-button");
10
+ const sampleButton = document.querySelector("#sample-button");
11
+ const copyButton = document.querySelector("#copy-button");
12
+ const refills = document.querySelector("#refills");
13
+ const schedule = document.querySelector("#schedule");
14
+ const cautions = document.querySelector("#cautions");
15
+ const totalMeds = document.querySelector("#total-meds");
16
+ const urgentCount = document.querySelector("#urgent-count");
17
+ const doseCount = document.querySelector("#dose-count");
18
+ const queueStatus = document.querySelector("#queue-status");
19
+
20
+ let currentPlan = null;
21
+
22
+ function esc(value) {
23
+ return String(value ?? "").replace(/[&<>"']/g, (char) => ({
24
+ "&": "&amp;",
25
+ "<": "&lt;",
26
+ ">": "&gt;",
27
+ '"': "&quot;",
28
+ "'": "&#039;",
29
+ })[char]);
30
+ }
31
+
32
+ function render(plan) {
33
+ currentPlan = plan;
34
+ totalMeds.textContent = plan.medications.length;
35
+ urgentCount.textContent = plan.refills.filter((item) => ["critical", "soon"].includes(item.urgency)).length;
36
+ doseCount.textContent = plan.schedule.length;
37
+ queueStatus.textContent = plan.refills[0]?.urgency === "critical" ? "Action needed" : "Stable";
38
+
39
+ refills.innerHTML = plan.refills.length ? plan.refills.map((item) => `
40
+ <article class="refill">
41
+ <div>
42
+ <div class="title">${esc(item.name)} <span class="meta">for ${esc(item.owner)}</span></div>
43
+ <div class="meta">${item.days_left ?? "Unknown"} days left${item.runout_date ? ` · runs out around ${esc(item.runout_date)}` : ""}</div>
44
+ <div class="meta">${esc(item.message)}</div>
45
+ </div>
46
+ <span class="badge ${esc(item.urgency)}">${esc(item.urgency)}</span>
47
+ </article>
48
+ `).join("") : `<div class="empty">No refill rows yet.</div>`;
49
+
50
+ schedule.innerHTML = plan.schedule.length ? plan.schedule.map((item) => `
51
+ <div class="timeline-row">
52
+ <div class="time">${esc(item.time)}</div>
53
+ <div class="dose">
54
+ <div class="title">${esc(item.medicine)} <span class="meta">${esc(item.strength)}</span></div>
55
+ <div class="meta">${esc(item.owner)} · ${esc(item.instructions)}</div>
56
+ </div>
57
+ </div>
58
+ `).join("") : `<div class="empty">No schedule could be inferred from the current rows.</div>`;
59
+
60
+ cautions.innerHTML = plan.cautions.length ? plan.cautions.map((item) => `
61
+ <article class="caution">
62
+ <div>
63
+ <div class="title">${esc(item.name)}</div>
64
+ <div class="meta">${esc(item.message)}</div>
65
+ </div>
66
+ <span class="badge scheduled">review</span>
67
+ </article>
68
+ `).join("") : `<div class="empty">No built-in review notes matched this list.</div>`;
69
+ }
70
+
71
+ async function buildPlan() {
72
+ planButton.disabled = true;
73
+ planButton.textContent = "Planning...";
74
+ try {
75
+ const response = await fetch("/api/plan", {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify({ medications: medications.value }),
79
+ });
80
+ const payload = await response.json();
81
+ if (!response.ok) throw new Error(payload.error || "Unable to build plan");
82
+ render(payload);
83
+ } catch (error) {
84
+ refills.innerHTML = `<div class="empty">${esc(error.message)}</div>`;
85
+ } finally {
86
+ planButton.disabled = false;
87
+ planButton.textContent = "Build plan";
88
+ }
89
+ }
90
+
91
+ function brief() {
92
+ if (!currentPlan) return "";
93
+ const refillLines = currentPlan.refills.map((item) => `- ${item.owner}: ${item.name} (${item.urgency}, ${item.days_left ?? "unknown"} days left)`);
94
+ const cautionLines = currentPlan.cautions.map((item) => `- ${item.name}: ${item.message}`);
95
+ return [`Dosewise Home brief`, "", "Refill queue:", ...refillLines, "", "Review notes:", ...cautionLines].join("\n");
96
+ }
97
+
98
+ planButton.addEventListener("click", buildPlan);
99
+ sampleButton.addEventListener("click", () => {
100
+ medications.value = sample;
101
+ buildPlan();
102
+ });
103
+ copyButton.addEventListener("click", async () => {
104
+ await navigator.clipboard.writeText(brief());
105
+ copyButton.textContent = "Copied";
106
+ setTimeout(() => copyButton.textContent = "Copy brief", 1200);
107
+ });
108
+
109
+ medications.value = sample;
110
+ buildPlan();
@@ -0,0 +1,77 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Dosewise Home</title>
7
+ <link rel="stylesheet" href="/styles.css" />
8
+ </head>
9
+ <body>
10
+ <main class="app">
11
+ <section class="workspace">
12
+ <div class="panel input-panel">
13
+ <div>
14
+ <p class="eyebrow">Local household planner</p>
15
+ <h1>Dosewise Home</h1>
16
+ <p class="lede">
17
+ Turn a medicine cabinet list into a same-week refill queue, dose timeline, and household safety notes.
18
+ </p>
19
+ </div>
20
+
21
+ <label for="medications">Medication list</label>
22
+ <textarea id="medications" spellcheck="false"></textarea>
23
+ <div class="actions">
24
+ <button id="plan-button">Build plan</button>
25
+ <button id="sample-button" class="secondary">Reload sample</button>
26
+ <button id="copy-button" class="secondary">Copy brief</button>
27
+ </div>
28
+ <p class="notice">
29
+ This app organizes household information. It does not diagnose, prescribe, or replace a pharmacist or clinician.
30
+ </p>
31
+ </div>
32
+
33
+ <div class="results">
34
+ <section class="summary-strip">
35
+ <div>
36
+ <span id="total-meds">0</span>
37
+ <small>medicines</small>
38
+ </div>
39
+ <div>
40
+ <span id="urgent-count">0</span>
41
+ <small>urgent refills</small>
42
+ </div>
43
+ <div>
44
+ <span id="dose-count">0</span>
45
+ <small>daily moments</small>
46
+ </div>
47
+ </section>
48
+
49
+ <section class="panel">
50
+ <div class="panel-head">
51
+ <h2>Refill queue</h2>
52
+ <span id="queue-status">Ready</span>
53
+ </div>
54
+ <div id="refills" class="stack"></div>
55
+ </section>
56
+
57
+ <section class="panel">
58
+ <div class="panel-head">
59
+ <h2>Today timeline</h2>
60
+ <span>Estimated from instructions</span>
61
+ </div>
62
+ <div id="schedule" class="timeline"></div>
63
+ </section>
64
+
65
+ <section class="panel">
66
+ <div class="panel-head">
67
+ <h2>Review notes</h2>
68
+ <span>Pharmacy conversation prompts</span>
69
+ </div>
70
+ <div id="cautions" class="stack"></div>
71
+ </section>
72
+ </div>
73
+ </section>
74
+ </main>
75
+ <script src="/app.js"></script>
76
+ </body>
77
+ </html>
@@ -0,0 +1,309 @@
1
+ :root {
2
+ color-scheme: light;
3
+ --ink: #20252b;
4
+ --muted: #65717e;
5
+ --line: #dce3e1;
6
+ --paper: #fbfaf6;
7
+ --panel: #ffffff;
8
+ --teal: #0f766e;
9
+ --blue: #245d8a;
10
+ --amber: #aa650c;
11
+ --red: #b42318;
12
+ --green: #28704a;
13
+ --shadow: 0 18px 50px rgba(31, 45, 50, 0.12);
14
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ body {
22
+ margin: 0;
23
+ background:
24
+ linear-gradient(120deg, rgba(15, 118, 110, 0.08), rgba(36, 93, 138, 0.06)),
25
+ var(--paper);
26
+ color: var(--ink);
27
+ }
28
+
29
+ .app {
30
+ min-height: 100vh;
31
+ padding: 28px;
32
+ }
33
+
34
+ .workspace {
35
+ display: grid;
36
+ grid-template-columns: minmax(320px, 430px) minmax(0, 1fr);
37
+ gap: 20px;
38
+ max-width: 1320px;
39
+ margin: 0 auto;
40
+ }
41
+
42
+ .panel,
43
+ .summary-strip {
44
+ background: var(--panel);
45
+ border: 1px solid var(--line);
46
+ border-radius: 8px;
47
+ box-shadow: var(--shadow);
48
+ }
49
+
50
+ .input-panel {
51
+ position: sticky;
52
+ top: 28px;
53
+ align-self: start;
54
+ padding: 24px;
55
+ display: grid;
56
+ gap: 16px;
57
+ }
58
+
59
+ .eyebrow {
60
+ color: var(--teal);
61
+ font-size: 0.78rem;
62
+ font-weight: 800;
63
+ letter-spacing: 0;
64
+ margin: 0 0 8px;
65
+ text-transform: uppercase;
66
+ }
67
+
68
+ h1,
69
+ h2 {
70
+ margin: 0;
71
+ letter-spacing: 0;
72
+ }
73
+
74
+ h1 {
75
+ font-size: 2.4rem;
76
+ line-height: 1;
77
+ }
78
+
79
+ h2 {
80
+ font-size: 1rem;
81
+ }
82
+
83
+ .lede,
84
+ .notice {
85
+ color: var(--muted);
86
+ line-height: 1.5;
87
+ }
88
+
89
+ .lede {
90
+ margin: 12px 0 0;
91
+ }
92
+
93
+ .notice {
94
+ margin: 0;
95
+ font-size: 0.88rem;
96
+ }
97
+
98
+ label {
99
+ font-weight: 750;
100
+ font-size: 0.9rem;
101
+ }
102
+
103
+ textarea {
104
+ width: 100%;
105
+ min-height: 390px;
106
+ resize: vertical;
107
+ border: 1px solid var(--line);
108
+ border-radius: 8px;
109
+ padding: 14px;
110
+ font: 0.9rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
111
+ color: var(--ink);
112
+ background: #fcfdfc;
113
+ }
114
+
115
+ .actions {
116
+ display: flex;
117
+ flex-wrap: wrap;
118
+ gap: 10px;
119
+ }
120
+
121
+ button {
122
+ min-height: 42px;
123
+ border: 0;
124
+ border-radius: 8px;
125
+ padding: 0 16px;
126
+ background: var(--teal);
127
+ color: white;
128
+ font-weight: 800;
129
+ cursor: pointer;
130
+ }
131
+
132
+ button.secondary {
133
+ background: #eef4f2;
134
+ color: var(--ink);
135
+ border: 1px solid var(--line);
136
+ }
137
+
138
+ .results {
139
+ display: grid;
140
+ gap: 18px;
141
+ }
142
+
143
+ .summary-strip {
144
+ display: grid;
145
+ grid-template-columns: repeat(3, 1fr);
146
+ overflow: hidden;
147
+ }
148
+
149
+ .summary-strip div {
150
+ padding: 20px;
151
+ border-right: 1px solid var(--line);
152
+ }
153
+
154
+ .summary-strip div:last-child {
155
+ border-right: 0;
156
+ }
157
+
158
+ .summary-strip span {
159
+ display: block;
160
+ font-size: 2rem;
161
+ font-weight: 850;
162
+ color: var(--blue);
163
+ }
164
+
165
+ .summary-strip small,
166
+ .panel-head span {
167
+ color: var(--muted);
168
+ font-weight: 650;
169
+ font-size: 0.82rem;
170
+ }
171
+
172
+ .panel {
173
+ padding: 20px;
174
+ }
175
+
176
+ .panel-head {
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: space-between;
180
+ gap: 14px;
181
+ margin-bottom: 14px;
182
+ }
183
+
184
+ .stack {
185
+ display: grid;
186
+ gap: 10px;
187
+ }
188
+
189
+ .refill,
190
+ .caution {
191
+ display: grid;
192
+ grid-template-columns: minmax(0, 1fr) auto;
193
+ gap: 12px;
194
+ padding: 14px;
195
+ border: 1px solid var(--line);
196
+ border-radius: 8px;
197
+ background: #fcfdfc;
198
+ }
199
+
200
+ .title {
201
+ font-weight: 850;
202
+ }
203
+
204
+ .meta {
205
+ color: var(--muted);
206
+ margin-top: 3px;
207
+ font-size: 0.88rem;
208
+ }
209
+
210
+ .badge {
211
+ align-self: start;
212
+ border-radius: 999px;
213
+ padding: 5px 10px;
214
+ font-size: 0.75rem;
215
+ font-weight: 850;
216
+ text-transform: uppercase;
217
+ }
218
+
219
+ .critical {
220
+ color: var(--red);
221
+ background: #fff0ee;
222
+ }
223
+
224
+ .soon,
225
+ .scheduled {
226
+ color: var(--amber);
227
+ background: #fff7e8;
228
+ }
229
+
230
+ .steady {
231
+ color: var(--green);
232
+ background: #ecf8f1;
233
+ }
234
+
235
+ .timeline {
236
+ display: grid;
237
+ gap: 0;
238
+ border: 1px solid var(--line);
239
+ border-radius: 8px;
240
+ overflow: hidden;
241
+ }
242
+
243
+ .timeline-row {
244
+ display: grid;
245
+ grid-template-columns: 82px minmax(0, 1fr);
246
+ min-height: 62px;
247
+ border-bottom: 1px solid var(--line);
248
+ }
249
+
250
+ .timeline-row:last-child {
251
+ border-bottom: 0;
252
+ }
253
+
254
+ .time {
255
+ display: grid;
256
+ place-items: center;
257
+ background: #f1f7f6;
258
+ color: var(--teal);
259
+ font-weight: 850;
260
+ }
261
+
262
+ .dose {
263
+ padding: 12px 14px;
264
+ }
265
+
266
+ .empty {
267
+ padding: 18px;
268
+ border: 1px dashed var(--line);
269
+ border-radius: 8px;
270
+ color: var(--muted);
271
+ }
272
+
273
+ @media (max-width: 920px) {
274
+ .app {
275
+ padding: 16px;
276
+ }
277
+
278
+ .workspace {
279
+ grid-template-columns: 1fr;
280
+ }
281
+
282
+ .input-panel {
283
+ position: static;
284
+ }
285
+ }
286
+
287
+ @media (max-width: 560px) {
288
+ h1 {
289
+ font-size: 2rem;
290
+ }
291
+
292
+ .summary-strip {
293
+ grid-template-columns: 1fr;
294
+ }
295
+
296
+ .summary-strip div {
297
+ border-right: 0;
298
+ border-bottom: 1px solid var(--line);
299
+ }
300
+
301
+ .summary-strip div:last-child {
302
+ border-bottom: 0;
303
+ }
304
+
305
+ .refill,
306
+ .caution {
307
+ grid-template-columns: 1fr;
308
+ }
309
+ }
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: dosewise-home
3
+ Version: 0.1.0
4
+ Summary: Local-first household medication schedule and refill planner
5
+ Author: Dosewise Home
6
+ License-Expression: MIT
7
+ Keywords: medication,refill,health,household,planner
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # Dosewise Home
18
+
19
+ Dosewise Home is a local-first household medication schedule and refill planner. Paste a simple medication cabinet CSV, then get a same-week refill queue, daily dose timeline, and pharmacist conversation prompts without creating an account or uploading private health notes.
20
+
21
+ ![Dosewise Home dashboard](assets/screenshots/dosewise-home.svg)
22
+
23
+ ## Features
24
+
25
+ - Runs locally from a small Python server and browser interface.
26
+ - Imports comma, tab, semicolon, or pipe-delimited medication rows.
27
+ - Estimates days left and prioritizes critical, soon, scheduled, and steady refills.
28
+ - Builds a daily dose timeline from common instruction words like morning, dinner, bedtime, and night.
29
+ - Flags plain-language review prompts for common household issues such as grapefruit cautions, duplicate acetaminophen, blood thinner review, and stacked sleep aids.
30
+ - Copies a concise refill brief for a caregiver, pharmacy call, or household planning note.
31
+
32
+ Dosewise Home organizes information only. It does not diagnose, prescribe, or replace medical advice from a clinician or pharmacist.
33
+
34
+ ## Installation
35
+
36
+ Python 3.10 or newer is required.
37
+
38
+ ```bash
39
+ git clone https://github.com/iwaheedsattar/dosewise-home.git
40
+ cd dosewise-home
41
+ python3 -m venv .venv
42
+ source .venv/bin/activate
43
+ python -m pip install -e .
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ Start the local app:
49
+
50
+ ```bash
51
+ dosewise-home
52
+ ```
53
+
54
+ Open [http://127.0.0.1:8765](http://127.0.0.1:8765), paste medication rows, and select **Build plan**.
55
+
56
+ Example CSV:
57
+
58
+ ```csv
59
+ name,owner,strength,instructions,quantity_left,doses_per_day,refill_date,notes
60
+ Atorvastatin,Sam,20mg,Take one tablet with dinner,12,1,2026-07-03,Avoid grapefruit
61
+ Lisinopril,Sam,10mg,Take every morning,24,1,2026-07-20,Blood pressure
62
+ Acetaminophen Cold,Lee,500mg,Take at bedtime if needed,4,2,2026-06-30,Cold medicine
63
+ ```
64
+
65
+ A fuller sample is available at `examples/household-medications.csv`.
66
+
67
+ ## CSV Columns
68
+
69
+ Required columns:
70
+
71
+ - `name`
72
+ - `owner`
73
+ - `strength`
74
+ - `instructions`
75
+ - `quantity_left`
76
+ - `doses_per_day`
77
+
78
+ Optional columns:
79
+
80
+ - `refill_date` in `YYYY-MM-DD`, `MM/DD/YYYY`, or `MM/DD/YY` format
81
+ - `notes`
82
+
83
+ ## Development
84
+
85
+ Run tests:
86
+
87
+ ```bash
88
+ python3 -m pytest
89
+ ```
90
+
91
+ Run without installing:
92
+
93
+ ```bash
94
+ python3 -m dosewise_home.server
95
+ ```
96
+
97
+ ## Roadmap
98
+
99
+ - Add printable weekly pill-box sheets.
100
+ - Add encrypted local browser storage for recurring household lists.
101
+ - Add optional OCR import from prescription labels.
102
+ - Add iCalendar reminders for refill windows.
103
+ - Package as a signed desktop application.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ dosewise_home/__init__.py
5
+ dosewise_home/planner.py
6
+ dosewise_home/server.py
7
+ dosewise_home.egg-info/PKG-INFO
8
+ dosewise_home.egg-info/SOURCES.txt
9
+ dosewise_home.egg-info/dependency_links.txt
10
+ dosewise_home.egg-info/entry_points.txt
11
+ dosewise_home.egg-info/top_level.txt
12
+ dosewise_home/web/app.js
13
+ dosewise_home/web/index.html
14
+ dosewise_home/web/styles.css
15
+ tests/test_planner.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dosewise-home = dosewise_home.server:main
@@ -0,0 +1 @@
1
+ dosewise_home
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dosewise-home"
7
+ version = "0.1.0"
8
+ description = "Local-first household medication schedule and refill planner"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "Dosewise Home" }]
12
+ license = "MIT"
13
+ keywords = ["medication", "refill", "health", "household", "planner"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Web Environment",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "Programming Language :: Python :: 3",
19
+ ]
20
+
21
+ [project.scripts]
22
+ dosewise-home = "dosewise_home.server:main"
23
+
24
+ [tool.setuptools.packages.find]
25
+ include = ["dosewise_home*"]
26
+
27
+ [tool.setuptools.package-data]
28
+ dosewise_home = ["web/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ from datetime import date
2
+
3
+ from dosewise_home.planner import build_plan, default_slots, parse_medications
4
+
5
+
6
+ SAMPLE = """name,owner,strength,instructions,quantity_left,doses_per_day,refill_date,notes
7
+ Atorvastatin,Sam,20mg,Take one tablet with dinner,12,1,2026-07-03,Avoid grapefruit
8
+ Acetaminophen Cold,Lee,500mg,Take at bedtime if needed,4,2,2026-06-30,Cold medicine
9
+ """
10
+
11
+
12
+ def test_parse_medications_reads_household_rows():
13
+ meds = parse_medications(SAMPLE)
14
+ assert len(meds) == 2
15
+ assert meds[0].owner == "Sam"
16
+ assert meds[1].quantity_left == 4
17
+
18
+
19
+ def test_build_plan_prioritizes_low_supply_and_cautions():
20
+ plan = build_plan(SAMPLE, today=date(2026, 6, 27))
21
+ assert plan["refills"][0]["name"] == "Acetaminophen Cold"
22
+ assert plan["refills"][0]["urgency"] == "critical"
23
+ assert {item["name"] for item in plan["cautions"]} == {
24
+ "Grapefruit caution",
25
+ "Duplicate acetaminophen",
26
+ }
27
+
28
+
29
+ def test_default_slots_match_daily_frequency():
30
+ assert [slot.strftime("%H:%M") for slot in default_slots(2)] == ["08:00", "20:00"]