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.
- dosewise_home-0.1.0/LICENSE +21 -0
- dosewise_home-0.1.0/PKG-INFO +103 -0
- dosewise_home-0.1.0/README.md +87 -0
- dosewise_home-0.1.0/dosewise_home/__init__.py +3 -0
- dosewise_home-0.1.0/dosewise_home/planner.py +209 -0
- dosewise_home-0.1.0/dosewise_home/server.py +50 -0
- dosewise_home-0.1.0/dosewise_home/web/app.js +110 -0
- dosewise_home-0.1.0/dosewise_home/web/index.html +77 -0
- dosewise_home-0.1.0/dosewise_home/web/styles.css +309 -0
- dosewise_home-0.1.0/dosewise_home.egg-info/PKG-INFO +103 -0
- dosewise_home-0.1.0/dosewise_home.egg-info/SOURCES.txt +15 -0
- dosewise_home-0.1.0/dosewise_home.egg-info/dependency_links.txt +1 -0
- dosewise_home-0.1.0/dosewise_home.egg-info/entry_points.txt +2 -0
- dosewise_home-0.1.0/dosewise_home.egg-info/top_level.txt +1 -0
- dosewise_home-0.1.0/pyproject.toml +28 -0
- dosewise_home-0.1.0/setup.cfg +4 -0
- dosewise_home-0.1.0/tests/test_planner.py +30 -0
|
@@ -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
|
+

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

|
|
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,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
|
+
"&": "&",
|
|
25
|
+
"<": "<",
|
|
26
|
+
">": ">",
|
|
27
|
+
'"': """,
|
|
28
|
+
"'": "'",
|
|
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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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,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"]
|