- The medicine database is now much bigger and has a real update path.

Added:
Live source updater for RxNorm, DailyMed, and openFDA.
Admin dashboard panel to refresh medicine sources.
CLI updater for periodic jobs: 
[scripts/update_medicine_catalog.php](/Users/tyemeclifford/Documents/GH/workout/scripts/update_medicine_catalog.php)
Update history table: medication_source_updates.
Curl fallback for external fetches when PHP stream DNS fails.
README instructions for periodic updates.
I also ran the imports:
Current medication catalog count: 6,002.
Latest source imports:RxNorm: 3,875 inserted, 1,125 updated.
openFDA: 912 inserted, 88 updated.
DailyMed: 95 inserted.

Periodic update command:
/Users/tyemeclifford/frankenphp php-cli 
scripts/update_medicine_catalog.php 5000 all
This commit is contained in:
Ty Clifford
2026-06-30 15:29:57 -04:00
parent 17ebaf21a3
commit 96de47be43
10 changed files with 594 additions and 14 deletions
+20 -2
View File
@@ -35,12 +35,13 @@ SQLite is created automatically at:
data/health_tracker.sqlite
```
The app seeds sample profiles, medical terminology, condition summaries, a medication catalog, NIH/NLM study-link categories, medication logs, vitals, labs, symptoms, allergies, appointments, immunizations, and the older optional wellness/fitness data.
The app seeds sample profiles, medical terminology, condition summaries, an expanded medication catalog, NIH/NLM study-link categories, medication logs, vitals, labs, symptoms, allergies, appointments, immunizations, and the older optional wellness/fitness data.
## Included
- Multi-user profiles with separate records and theme preferences.
- Admin dashboard for creating, editing, activating/deactivating, and deleting user profiles.
- Admin medicine-source updater for refreshing the catalog from RxNorm, DailyMed, and openFDA.
- First user created in a database is automatically promoted to `admin`; later users default to `member`.
- Daily, weekly, and monthly views for medical logs.
- Medication logging by date, dose, route, frequency, status, prescriber, and notes.
@@ -48,14 +49,31 @@ The app seeds sample profiles, medical terminology, condition summaries, a medic
- Lab result records with units, reference ranges, clinician/source notes, and collection dates.
- Symptom journal, allergy list, appointments, and immunization records.
- Medical terminology and condition database with source links.
- Medicine catalog with RxNorm, MedlinePlus, PubMed, and ClinicalTrials.gov links.
- Searchable medicine catalog by generic name, active ingredient, chemical name, common brand names, and generic/alternate names.
- Medicine links to RxNorm, DailyMed, PubChem, MedlinePlus, PubMed, and ClinicalTrials.gov.
- NIH/NLM research-link categories related to the listed medicines.
- Neon dark theme by default, light mode option, and green, blue, red, pink, and orange accents.
- Optional legacy fitness, nutrition, and supplement tracking remains available as wellness context.
## Medicine Catalog Updates
The catalog ships with a curated starter set, the current SQLite file includes thousands of imported medicine concepts, and it can be refreshed from public medicine sources.
From the admin dashboard, use the medicine source updater and choose `All sources`, `RxNorm`, `DailyMed`, or `openFDA`.
From the command line:
```sh
/Users/tyemeclifford/frankenphp php-cli scripts/update_medicine_catalog.php 1000 all
```
For a periodic cron-style update, run the same command nightly or weekly. The first argument is the max rows per source, from `25` to `5000`; the second argument is `all`, `RxNorm`, `DailyMed`, or `openFDA`.
## Reference Sources
- RxNorm and RxNav from the U.S. National Library of Medicine: `https://www.nlm.nih.gov/research/umls/rxnorm/index.html`
- DailyMed label search from the U.S. National Library of Medicine: `https://dailymed.nlm.nih.gov/dailymed/`
- PubChem chemical/compound search from NCBI: `https://pubchem.ncbi.nlm.nih.gov/`
- MedlinePlus condition and medication pages: `https://medlineplus.gov/`
- PubMed search links for medicine and condition research: `https://pubmed.ncbi.nlm.nih.gov/`
- ClinicalTrials.gov search links for study discovery: `https://clinicaltrials.gov/`
+324
View File
@@ -23,6 +23,7 @@ function app_db(): PDO
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('PRAGMA foreign_keys = ON');
$pdo->exec('PRAGMA busy_timeout = 5000');
app_init_db($pdo);
@@ -315,6 +316,16 @@ CREATE TABLE IF NOT EXISTS research_studies (
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS medication_source_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
status TEXT NOT NULL,
imported_count INTEGER NOT NULL DEFAULT 0,
updated_count INTEGER NOT NULL DEFAULT 0,
message TEXT DEFAULT '',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_workouts_user_date ON workouts(user_id, scheduled_on);
CREATE INDEX IF NOT EXISTS idx_nutrition_logs_user_date ON nutrition_logs(user_id, logged_on);
CREATE INDEX IF NOT EXISTS idx_supplement_logs_user_date ON supplement_logs(user_id, taken_on);
@@ -328,6 +339,7 @@ CREATE INDEX IF NOT EXISTS idx_appointments_user_date ON appointments(user_id, a
CREATE INDEX IF NOT EXISTS idx_immunization_records_user_date ON immunization_records(user_id, administered_on);
CREATE INDEX IF NOT EXISTS idx_medical_terms_term ON medical_terms(term);
CREATE INDEX IF NOT EXISTS idx_medication_catalog_generic ON medication_catalog(generic_name);
CREATE INDEX IF NOT EXISTS idx_medication_source_updates_created ON medication_source_updates(created_at);
SQL);
migrate_users_table($pdo);
@@ -611,6 +623,12 @@ function seed_expanded_medication_catalog(PDO $pdo): void
{
$created = app_now();
$rows = expanded_medication_catalog_rows();
$existingCount = (int) $pdo->query('SELECT COUNT(*) FROM medication_catalog')->fetchColumn();
$needsSearchText = (int) $pdo->query("SELECT COUNT(*) FROM medication_catalog WHERE search_terms IS NULL OR search_terms = ''")->fetchColumn();
$needsChemicalNames = (int) $pdo->query("SELECT COUNT(*) FROM medication_catalog WHERE chemical_name IS NULL OR chemical_name = ''")->fetchColumn();
if ($existingCount >= count($rows) && $needsSearchText === 0 && $needsChemicalNames === 0) {
return;
}
$select = $pdo->prepare('SELECT id FROM medication_catalog WHERE lower(generic_name) = lower(?) LIMIT 1');
$insert = $pdo->prepare(
@@ -1705,6 +1723,312 @@ function library_search_payload(array $query): array
];
}
function http_json(string $url, int $timeout = 20): ?array
{
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'header' => "Accept: application/json\r\nUser-Agent: NeonMedicalTracker/1.0\r\n",
],
]);
$body = @file_get_contents($url, false, $context);
if (($body === false || $body === '') && function_exists('shell_exec')) {
$curl = trim((string) shell_exec('command -v curl 2>/dev/null'));
if ($curl === '' && is_executable('/usr/bin/curl')) {
$curl = '/usr/bin/curl';
}
if ($curl !== '') {
$command = escapeshellarg($curl)
. ' -fsSL --max-time ' . (int) $timeout
. ' -H ' . escapeshellarg('Accept: application/json')
. ' -A ' . escapeshellarg('NeonMedicalTracker/1.0')
. ' ' . escapeshellarg($url)
. ' 2>/dev/null';
$body = shell_exec($command);
}
}
if (!is_string($body) || $body === '') {
return null;
}
$json = json_decode($body, true);
return is_array($json) ? $json : null;
}
function medication_import_row(string $name, string $source, string $brandNames = '', string $className = 'Imported medicine concept'): array
{
$cleanName = preg_replace('/\s+/', ' ', trim($name));
if ($cleanName === '') {
return [];
}
$sourceLabel = match ($source) {
'RxNorm' => 'RxNorm / NLM',
'DailyMed' => 'DailyMed / NLM',
'openFDA' => 'openFDA / FDA',
default => $source,
};
return [
'generic_name' => strtolower($cleanName),
'active_ingredient' => $cleanName,
'chemical_name' => $cleanName,
'brand_names' => $brandNames,
'generic_brands' => $cleanName,
'class_name' => $className,
'drug_family' => 'Imported reference',
'common_use' => "Imported from {$sourceLabel}. Review official labels and clinician guidance before use.",
'tracking_note' => 'Use this entry as a reference/search starting point; verify dose, indication, and safety details with official labeling or a clinician.',
'rxnorm_url' => medication_source_url('https://mor.nlm.nih.gov/RxNav/search?searchBy=String&searchTerm=', $cleanName),
'medlineplus_url' => 'https://medlineplus.gov/druginformation.html',
'pubchem_url' => medication_source_url('https://pubchem.ncbi.nlm.nih.gov/#query=', $cleanName),
'dailymed_url' => medication_source_url('https://dailymed.nlm.nih.gov/dailymed/search.cfm?query=', $cleanName),
'pubmed_url' => medication_source_url('https://pubmed.ncbi.nlm.nih.gov/?term=', $cleanName . ' medication'),
'clinicaltrials_url' => medication_source_url('https://clinicaltrials.gov/search?term=', $cleanName),
'source_name' => $sourceLabel,
'search_terms' => implode(' ', [$cleanName, $brandNames, $className, $sourceLabel]),
];
}
function fetch_rxnorm_medication_rows(int $limit): array
{
$rows = [];
foreach (['IN', 'PIN'] as $tty) {
$json = http_json('https://rxnav.nlm.nih.gov/REST/allconcepts.json?tty=' . $tty);
$concepts = $json['minConceptGroup']['minConcept'] ?? [];
if (!is_array($concepts)) {
continue;
}
foreach ($concepts as $concept) {
if (count($rows) >= $limit) {
break 2;
}
$name = (string) ($concept['name'] ?? '');
$row = medication_import_row($name, 'RxNorm');
if ($row) {
$rows[] = $row;
}
}
}
return $rows;
}
function fetch_openfda_medication_rows(int $limit): array
{
$url = 'https://api.fda.gov/drug/label.json?count=openfda.generic_name.exact&limit=' . $limit;
$json = http_json($url);
$results = $json['results'] ?? [];
if (!is_array($results)) {
return [];
}
$rows = [];
foreach ($results as $result) {
if (count($rows) >= $limit) {
break;
}
$term = (string) ($result['term'] ?? '');
$row = medication_import_row($term, 'openFDA');
if ($row) {
$rows[] = $row;
}
}
return $rows;
}
function fetch_dailymed_medication_rows(int $limit): array
{
$url = 'https://dailymed.nlm.nih.gov/dailymed/services/v2/drugnames.json';
$json = http_json($url);
$data = $json['data'] ?? $json['drugNames'] ?? $json['results'] ?? [];
if (!is_array($data)) {
return [];
}
$rows = [];
foreach ($data as $item) {
if (count($rows) >= $limit) {
break;
}
$name = is_array($item)
? (string) ($item['drug_name'] ?? $item['name'] ?? $item['term'] ?? '')
: (string) $item;
$row = medication_import_row($name, 'DailyMed');
if ($row) {
$rows[] = $row;
}
}
return $rows;
}
function merge_text_values(string $current, string $incoming): string
{
$parts = [];
foreach (explode(',', $current . ',' . $incoming) as $part) {
$value = trim($part);
if ($value === '') {
continue;
}
$parts[strtolower($value)] = $value;
}
return implode(', ', array_values($parts));
}
function upsert_imported_medication_rows(PDO $pdo, array $rows): array
{
$created = app_now();
$select = $pdo->prepare('SELECT * FROM medication_catalog WHERE lower(generic_name) = lower(?) LIMIT 1');
$insert = $pdo->prepare(
"INSERT INTO medication_catalog
(generic_name, active_ingredient, chemical_name, brand_names, generic_brands, class_name, drug_family,
common_use, tracking_note, rxnorm_url, medlineplus_url, pubchem_url, dailymed_url, pubmed_url,
clinicaltrials_url, source_name, search_terms, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$update = $pdo->prepare(
"UPDATE medication_catalog
SET active_ingredient = ?, chemical_name = ?, brand_names = ?, generic_brands = ?,
class_name = ?, drug_family = ?, common_use = ?, tracking_note = ?, rxnorm_url = ?,
medlineplus_url = ?, pubchem_url = ?, dailymed_url = ?, pubmed_url = ?,
clinicaltrials_url = ?, source_name = ?, search_terms = ?
WHERE id = ?"
);
$seen = [];
$inserted = 0;
$updated = 0;
foreach ($rows as $row) {
if (!$row || empty($row['generic_name'])) {
continue;
}
$key = strtolower((string) $row['generic_name']);
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$select->execute([$row['generic_name']]);
$existing = $select->fetch();
if (!$existing) {
$insert->execute([
$row['generic_name'],
$row['active_ingredient'],
$row['chemical_name'],
$row['brand_names'],
$row['generic_brands'],
$row['class_name'],
$row['drug_family'],
$row['common_use'],
$row['tracking_note'],
$row['rxnorm_url'],
$row['medlineplus_url'],
$row['pubchem_url'],
$row['dailymed_url'],
$row['pubmed_url'],
$row['clinicaltrials_url'],
$row['source_name'],
$row['search_terms'],
$created,
]);
$inserted++;
continue;
}
$next = [];
foreach (['active_ingredient', 'chemical_name', 'class_name', 'drug_family', 'common_use', 'tracking_note', 'rxnorm_url', 'medlineplus_url', 'pubchem_url', 'dailymed_url', 'pubmed_url', 'clinicaltrials_url'] as $field) {
$next[$field] = trim((string) ($existing[$field] ?? '')) !== '' ? $existing[$field] : $row[$field];
}
$next['brand_names'] = merge_text_values((string) ($existing['brand_names'] ?? ''), (string) $row['brand_names']);
$next['generic_brands'] = merge_text_values((string) ($existing['generic_brands'] ?? ''), (string) $row['generic_brands']);
$next['source_name'] = merge_text_values((string) ($existing['source_name'] ?? ''), (string) $row['source_name']);
$next['search_terms'] = trim((string) ($existing['search_terms'] ?? '') . ' ' . (string) $row['search_terms']);
$update->execute([
$next['active_ingredient'],
$next['chemical_name'],
$next['brand_names'],
$next['generic_brands'],
$next['class_name'],
$next['drug_family'],
$next['common_use'],
$next['tracking_note'],
$next['rxnorm_url'],
$next['medlineplus_url'],
$next['pubchem_url'],
$next['dailymed_url'],
$next['pubmed_url'],
$next['clinicaltrials_url'],
$next['source_name'],
$next['search_terms'],
(int) $existing['id'],
]);
$updated++;
}
return ['inserted' => $inserted, 'updated' => $updated, 'seen' => count($seen)];
}
function record_medication_source_update(PDO $pdo, string $source, string $status, int $inserted, int $updated, string $message): void
{
$stmt = $pdo->prepare(
"INSERT INTO medication_source_updates
(source, status, imported_count, updated_count, message, created_at)
VALUES (?, ?, ?, ?, ?, ?)"
);
$stmt->execute([$source, $status, $inserted, $updated, $message, app_now()]);
}
function refresh_medication_sources(array $data): array
{
$pdo = app_db();
if (array_key_exists('acting_user_id', $data) || array_key_exists('user_id', $data)) {
require_admin($pdo, $data);
}
$limit = max(25, min(5000, int_value($data, 'limit', 1000)));
$sources = [
'RxNorm' => 'fetch_rxnorm_medication_rows',
'DailyMed' => 'fetch_dailymed_medication_rows',
'openFDA' => 'fetch_openfda_medication_rows',
];
$requestedSource = text_value($data, 'source', 'all');
if ($requestedSource !== 'all') {
$sources = array_intersect_key($sources, [$requestedSource => true]);
}
$summary = [];
$totalInserted = 0;
$totalUpdated = 0;
foreach ($sources as $source => $fetcher) {
try {
$rows = $fetcher($limit);
if (!$rows) {
record_medication_source_update($pdo, $source, 'empty', 0, 0, 'No rows returned. The source may be unavailable or rate-limited.');
$summary[] = ['source' => $source, 'status' => 'empty', 'inserted' => 0, 'updated' => 0, 'message' => 'No rows returned.'];
continue;
}
$result = upsert_imported_medication_rows($pdo, $rows);
$totalInserted += $result['inserted'];
$totalUpdated += $result['updated'];
$message = "Fetched {$result['seen']} unique rows.";
record_medication_source_update($pdo, $source, 'ok', $result['inserted'], $result['updated'], $message);
$summary[] = ['source' => $source, 'status' => 'ok', 'inserted' => $result['inserted'], 'updated' => $result['updated'], 'message' => $message];
} catch (Throwable $exception) {
record_medication_source_update($pdo, $source, 'error', 0, 0, $exception->getMessage());
$summary[] = ['source' => $source, 'status' => 'error', 'inserted' => 0, 'updated' => 0, 'message' => $exception->getMessage()];
}
}
return [
'inserted' => $totalInserted,
'updated' => $totalUpdated,
'catalog_count' => (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_catalog'),
'sources' => $summary,
'recent_updates' => db_all($pdo, 'SELECT * FROM medication_source_updates ORDER BY created_at DESC, id DESC LIMIT 10'),
];
}
function create_user(array $data): array
{
$pdo = app_db();
Binary file not shown.
+15
View File
@@ -31,6 +31,21 @@ require __DIR__ . '/partials/site-header.php';
<section class="admin-panel site-admin-panel" id="adminPanel" hidden>
<div class="admin-stats" id="adminStats"></div>
<section class="admin-source-panel">
<div class="site-section-heading">
<p class="site-eyebrow">Medicine source updater</p>
<h2>Refresh the medication catalog from public sources.</h2>
<p>Pull medicine concepts from RxNorm, DailyMed, and openFDA. Use a smaller limit for quick checks or a larger limit for periodic maintenance.</p>
</div>
<form class="admin-source-form" id="medicineSourceRefreshForm">
<label class="site-field"><span>Source</span><select name="source"><option value="all">All sources</option><option>RxNorm</option><option>DailyMed</option><option>openFDA</option></select></label>
<label class="site-field"><span>Max rows per source</span><input name="limit" type="number" min="25" max="5000" step="25" value="1000"></label>
<button class="site-primary" type="submit">Refresh medicines</button>
</form>
<div class="source-refresh-result" id="medicineSourceRefreshResult" role="status" aria-live="polite"></div>
</section>
<div class="admin-layout">
<div class="admin-user-list" id="adminUserList"></div>
<form class="admin-editor" id="adminUserForm">
+1
View File
@@ -45,6 +45,7 @@ try {
'create_allergy_record' => 'create_allergy_record',
'create_appointment' => 'create_appointment',
'create_immunization_record' => 'create_immunization_record',
'refresh_medication_sources' => 'refresh_medication_sources',
];
if (!isset($routes[$action])) {
+37
View File
@@ -186,6 +186,22 @@ function adminFormPayload(form) {
return payload;
}
function renderRefreshResult(result) {
const target = admin$("#medicineSourceRefreshResult");
if (!target) return;
const rows = (result.sources || []).map((source) => `
<article>
<strong>${adminEscape(source.source)}: ${adminEscape(source.status)}</strong>
<span>${adminNumber(source.inserted)} inserted / ${adminNumber(source.updated)} updated</span>
<small>${adminEscape(source.message || "")}</small>
</article>
`).join("");
target.innerHTML = `
<p>${adminNumber(result.catalog_count)} medicines in catalog after refresh.</p>
<div class="source-refresh-list">${rows}</div>
`;
}
document.addEventListener("DOMContentLoaded", async () => {
admin$("#adminProfileSelect").addEventListener("change", async (event) => {
adminState.userId = event.target.value;
@@ -248,6 +264,27 @@ document.addEventListener("DOMContentLoaded", async () => {
}
});
const refreshForm = admin$("#medicineSourceRefreshForm");
if (refreshForm) {
refreshForm.addEventListener("submit", async (event) => {
event.preventDefault();
const payload = adminFormPayload(refreshForm);
const button = refreshForm.querySelector("button[type='submit']");
if (button) button.disabled = true;
admin$("#medicineSourceRefreshResult").textContent = "Refreshing medicine sources...";
try {
const result = await adminApi("refresh_medication_sources", payload);
renderRefreshResult(result);
adminToast("Medicine sources refreshed");
} catch (error) {
admin$("#medicineSourceRefreshResult").textContent = error.message;
adminToast(error.message);
} finally {
if (button) button.disabled = false;
}
});
}
try {
await loadAdminState();
} catch (error) {
+106 -1
View File
@@ -394,6 +394,64 @@ figcaption a {
font-size: 0.78rem;
}
.library-search {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 10px;
align-items: end;
margin: 12px 0 18px;
}
.library-search label {
display: grid;
gap: 8px;
color: var(--muted);
font-size: 0.82rem;
font-weight: 700;
}
.library-search input {
width: 100%;
min-height: 48px;
border: 1px solid var(--line);
border-radius: 8px;
background: color-mix(in srgb, var(--panel-2), transparent 6%);
color: var(--text);
padding: 12px 14px;
font: inherit;
}
.library-search input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent), transparent 76%);
outline: none;
}
.library-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-bottom: 18px;
}
.library-stats article {
border: 1px solid var(--line);
border-radius: var(--radius);
background: color-mix(in srgb, var(--panel-2), transparent 6%);
padding: 12px;
}
.library-stats strong {
display: block;
color: var(--accent);
font-size: 1.4rem;
}
.library-stats span {
color: var(--muted);
font-size: 0.82rem;
}
.site-split,
.detail-hero,
.signup-layout {
@@ -555,6 +613,47 @@ textarea:focus {
font-size: 1.5rem;
}
.admin-source-panel {
border: 1px solid var(--line);
border-radius: var(--radius);
background: color-mix(in srgb, var(--panel-2), transparent 5%);
padding: 14px;
margin-bottom: 14px;
}
.admin-source-form {
display: grid;
grid-template-columns: minmax(0, 1fr) 190px auto;
gap: 10px;
align-items: end;
}
.source-refresh-result {
color: var(--muted);
font-size: 0.88rem;
margin-top: 12px;
}
.source-refresh-list {
display: grid;
gap: 8px;
margin-top: 10px;
}
.source-refresh-list article {
border: 1px solid var(--line);
border-radius: 8px;
background: color-mix(in srgb, var(--panel), transparent 10%);
display: grid;
gap: 4px;
padding: 10px;
}
.source-refresh-list span,
.source-refresh-list small {
color: var(--muted);
}
.admin-layout {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr);
@@ -718,7 +817,8 @@ textarea:focus {
.site-split,
.detail-hero,
.signup-layout,
.admin-layout {
.admin-layout,
.admin-source-form {
grid-template-columns: 1fr;
}
@@ -753,11 +853,16 @@ textarea:focus {
.feature-grid,
.detail-grid,
.admin-stats,
.library-stats,
.signup-row,
.form-grid.two {
grid-template-columns: 1fr;
}
.library-search {
grid-template-columns: 1fr;
}
.admin-access-row,
.admin-user-main,
.panel-heading {
+35 -8
View File
@@ -6,19 +6,40 @@ $pdo = app_db();
$pageTitle = 'Medical Terminology Database | Neon Medical Tracker';
$activePage = 'terminology';
$terms = db_all($pdo, 'SELECT * FROM medical_terms ORDER BY category, term');
$conditions = db_all($pdo, 'SELECT * FROM medical_conditions ORDER BY category, name');
$medicines = db_all($pdo, 'SELECT * FROM medication_catalog ORDER BY generic_name');
$studies = db_all($pdo, 'SELECT * FROM research_studies ORDER BY category, related_medicine, title');
$searchQuery = trim((string) ($_GET['q'] ?? ''));
$library = library_search_payload(['q' => $searchQuery, 'limit' => 500]);
$terms = $library['medicalTerms'];
$conditions = $library['medicalConditions'];
$medicines = $library['medicationCatalog'];
$studies = $library['researchStudies'];
$totalMedicines = (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_catalog');
require __DIR__ . '/partials/site-header.php';
?>
<main>
<section class="admin-public-shell">
<div class="site-section-heading">
<p class="site-eyebrow">Medical terminology database</p>
<h1>Terms, conditions, medicines, and NIH/NLM study links.</h1>
<p>Entries are concise app-authored summaries with links to NIH/NLM systems such as MedlinePlus, RxNorm, PubMed, and ClinicalTrials.gov. This is a tracking/reference aid, not medical advice.</p>
<p class="site-eyebrow">Searchable medical library</p>
<h1>Search terms, conditions, medicines, chemical names, and brands.</h1>
<p>Entries are concise app-authored summaries with links to NIH/NLM systems such as MedlinePlus, RxNorm, DailyMed, PubChem, PubMed, and ClinicalTrials.gov. This is a tracking/reference aid, not medical advice.</p>
</div>
<form class="library-search" method="get" action="/medical.php">
<label>
<span>Search medical library</span>
<input name="q" value="<?= htmlspecialchars($searchQuery, ENT_QUOTES) ?>" placeholder="Try Lipitor, atorvastatin, blood pressure, CBC, metformin">
</label>
<button class="site-primary" type="submit">Search</button>
<?php if ($searchQuery !== ''): ?>
<a class="site-secondary" href="/medical.php">Clear</a>
<?php endif; ?>
</form>
<div class="library-stats" aria-label="Search result counts">
<article><strong><?= count($medicines) ?></strong><span><?= $searchQuery === '' ? 'medicines shown of ' . number_format($totalMedicines) : 'medicine results' ?></span></article>
<article><strong><?= count($terms) ?></strong><span>terms</span></article>
<article><strong><?= count($conditions) ?></strong><span>conditions</span></article>
<article><strong><?= count($studies) ?></strong><span>study links</span></article>
</div>
<div class="detail-grid">
@@ -57,17 +78,23 @@ require __DIR__ . '/partials/site-header.php';
<section class="admin-public-shell">
<div class="site-section-heading">
<p class="site-eyebrow">Medication catalog</p>
<h2>Medicines with RxNorm, PubMed, and ClinicalTrials.gov links</h2>
<h2>Medicines with chemical, brand, RxNorm, DailyMed, and PubChem links</h2>
</div>
<div class="detail-grid">
<?php foreach ($medicines as $medicine): ?>
<article>
<h2><?= htmlspecialchars(ucfirst($medicine['generic_name']), ENT_QUOTES) ?></h2>
<p><strong><?= htmlspecialchars($medicine['class_name'], ENT_QUOTES) ?></strong></p>
<p><strong>Active ingredient:</strong> <?= htmlspecialchars($medicine['active_ingredient'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
<p><strong>Chemical name:</strong> <?= htmlspecialchars($medicine['chemical_name'] ?: $medicine['active_ingredient'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
<p><strong>Common brands:</strong> <?= htmlspecialchars($medicine['brand_names'] ?: 'Brand names vary by market', ENT_QUOTES) ?></p>
<p><strong>Generic/alternate names:</strong> <?= htmlspecialchars($medicine['generic_brands'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
<p><?= htmlspecialchars($medicine['common_use'], ENT_QUOTES) ?></p>
<p><?= htmlspecialchars($medicine['tracking_note'], ENT_QUOTES) ?></p>
<div class="source-links">
<a href="<?= htmlspecialchars($medicine['rxnorm_url'], ENT_QUOTES) ?>">RxNorm</a>
<a href="<?= htmlspecialchars($medicine['pubchem_url'], ENT_QUOTES) ?>">PubChem</a>
<a href="<?= htmlspecialchars($medicine['dailymed_url'], ENT_QUOTES) ?>">DailyMed</a>
<a href="<?= htmlspecialchars($medicine['medlineplus_url'], ENT_QUOTES) ?>">MedlinePlus</a>
<a href="<?= htmlspecialchars($medicine['pubmed_url'], ENT_QUOTES) ?>">PubMed</a>
<a href="<?= htmlspecialchars($medicine['clinicaltrials_url'], ENT_QUOTES) ?>">Studies</a>
+28 -3
View File
@@ -6,7 +6,10 @@ $pdo = app_db();
$pageTitle = 'Medicine Catalog | Neon Medical Tracker';
$activePage = 'medicines';
$medicines = db_all($pdo, 'SELECT * FROM medication_catalog ORDER BY generic_name');
$searchQuery = trim((string) ($_GET['q'] ?? ''));
$library = library_search_payload(['q' => $searchQuery, 'limit' => 500]);
$medicines = $library['medicationCatalog'];
$totalMedicines = (int) db_value($pdo, 'SELECT COUNT(*) FROM medication_catalog');
$photo = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/A_large_mixed_salad.jpg/1024px-A_large_mixed_salad.jpg';
require __DIR__ . '/partials/site-header.php';
?>
@@ -21,21 +24,43 @@ require __DIR__ . '/partials/site-header.php';
</figure>
<div>
<p class="site-eyebrow">Medicine catalog</p>
<h1>Track medicines with source links beside every catalog entry.</h1>
<p>The starter catalog includes common medicines and links each one to RxNorm, MedlinePlus, PubMed, and ClinicalTrials.gov searches.</p>
<h1>Search medicines by generic, chemical, brand, and alternate names.</h1>
<p>The catalog includes common medicines and links each one to RxNorm, DailyMed, PubChem, MedlinePlus, PubMed, and ClinicalTrials.gov searches.</p>
<a class="site-primary" href="/tracker.php">Open medical tracker</a>
</div>
</section>
<section class="admin-public-shell">
<form class="library-search" method="get" action="/nutrition.php">
<label>
<span>Search medicine catalog</span>
<input name="q" value="<?= htmlspecialchars($searchQuery, ENT_QUOTES) ?>" placeholder="Try Zoloft, sertraline, insulin, acetaminophen, blood pressure">
</label>
<button class="site-primary" type="submit">Search</button>
<?php if ($searchQuery !== ''): ?>
<a class="site-secondary" href="/nutrition.php">Clear</a>
<?php endif; ?>
</form>
<div class="library-stats" aria-label="Medicine result count">
<article><strong><?= count($medicines) ?></strong><span><?= $searchQuery === '' ? 'shown of ' . number_format($totalMedicines) . ' medicines' : 'medicine results' ?></span></article>
</div>
</section>
<section class="detail-grid">
<?php foreach ($medicines as $medicine): ?>
<article>
<h2><?= htmlspecialchars(ucfirst($medicine['generic_name']), ENT_QUOTES) ?></h2>
<p><strong><?= htmlspecialchars($medicine['class_name'], ENT_QUOTES) ?></strong></p>
<p><strong>Active ingredient:</strong> <?= htmlspecialchars($medicine['active_ingredient'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
<p><strong>Chemical name:</strong> <?= htmlspecialchars($medicine['chemical_name'] ?: $medicine['active_ingredient'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
<p><strong>Common brands:</strong> <?= htmlspecialchars($medicine['brand_names'] ?: 'Brand names vary by market', ENT_QUOTES) ?></p>
<p><strong>Generic/alternate names:</strong> <?= htmlspecialchars($medicine['generic_brands'] ?: $medicine['generic_name'], ENT_QUOTES) ?></p>
<p><?= htmlspecialchars($medicine['common_use'], ENT_QUOTES) ?></p>
<p><?= htmlspecialchars($medicine['tracking_note'], ENT_QUOTES) ?></p>
<div class="source-links">
<a href="<?= htmlspecialchars($medicine['rxnorm_url'], ENT_QUOTES) ?>">RxNorm</a>
<a href="<?= htmlspecialchars($medicine['pubchem_url'], ENT_QUOTES) ?>">PubChem</a>
<a href="<?= htmlspecialchars($medicine['dailymed_url'], ENT_QUOTES) ?>">DailyMed</a>
<a href="<?= htmlspecialchars($medicine['medlineplus_url'], ENT_QUOTES) ?>">MedlinePlus</a>
<a href="<?= htmlspecialchars($medicine['pubmed_url'], ENT_QUOTES) ?>">PubMed</a>
<a href="<?= htmlspecialchars($medicine['clinicaltrials_url'], ENT_QUOTES) ?>">Studies</a>
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../app/bootstrap.php';
if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "This script is intended for CLI use.\n");
exit(1);
}
$limit = isset($argv[1]) ? (int) $argv[1] : 1000;
$source = $argv[2] ?? 'all';
$result = refresh_medication_sources([
'limit' => $limit,
'source' => $source,
]);
echo "Medication catalog refresh complete\n";
echo "Catalog count: {$result['catalog_count']}\n";
echo "Inserted: {$result['inserted']}\n";
echo "Updated: {$result['updated']}\n";
foreach ($result['sources'] as $sourceResult) {
echo "- {$sourceResult['source']}: {$sourceResult['status']} ";
echo "({$sourceResult['inserted']} inserted, {$sourceResult['updated']} updated) ";
echo "{$sourceResult['message']}\n";
}