oddb2xml 3.0.25 → 3.0.26
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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/CLAUDE.md +15 -2
- data/Elexis_Artikelstamm_v6.xsd +559 -0
- data/Gemfile.lock +1 -1
- data/History.txt +3 -0
- data/README.md +58 -1
- data/lib/oddb2xml/builder.rb +51 -11
- data/lib/oddb2xml/cli.rb +4 -4
- data/lib/oddb2xml/fhir_support.rb +6 -0
- data/lib/oddb2xml/options.rb +1 -1
- data/lib/oddb2xml/version.rb +1 -1
- data/scripts/generate_index_html.sh +202 -0
- data/scripts/run_oddb2xml.sh +38 -1
- data/scripts/setup_mediupdatexml_web.sh +110 -0
- data/scripts/swissmedic_watch.sh +85 -0
- data/scripts/visitor_stats.py +324 -0
- data/spec/artikelstamm_spec.rb +14 -14
- data/spec/fhir_spec.rb +47 -0
- metadata +6 -1
data/History.txt
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
=== 3.0.26 / 29.06.2026
|
|
2
|
+
* New (--artikelstamm): emit the Elexis Artikelstamm v6 format with per-article BAG Indikationscodes (INDC). The output bumps from v5 to v6 -- namespace http://elexis.ch/Elexis_Artikelstamm_v6, file artikelstamm_DDMMYYYY_v6.xml/.csv, validated against the new bundled Elexis_Artikelstamm_v6.xsd -- and every SL price-model <ITEM> now carries an <ARTSL> block: <PM>true</PM> plus one <ARTLIM> per limitation with <LIMCD> (BAG limitation code), <INDCD> (the Indikationscode XXXXX.NN), and the validity dates <VDAT>/<VTDAT>. The indication codes were already read from the BAG SL FHIR feed's indicationCode extension (used since 3.0.7 for oddb_product.xml's <INDICATION_CODE>); they are now also carried per package limitation and grouped onto each article. Mandatory on prescriptions/invoices for SL price-model drugs from 2026-07-01 (issue #113). The v6 XSD is the canonical MEDEVIT schema extended with oddb2xml's historical Italian elements (DSCRI / DOSAGE_FORMI) so the output keeps validating. Consumers must switch from the v5 to the v6 file.
|
|
3
|
+
|
|
1
4
|
=== 3.0.25 / 17.06.2026
|
|
2
5
|
* Bugfix (-b/--firstbase): the GS1 Switzerland firstbase CSV (id.gs1.ch) is served with a UTF-8 BOM. FirstbaseExtractor read it with encoding "UTF-8", so the BOM glued onto the first header ("Gtin") and row["Gtin"] returned nil for every row — every line was skipped as having an empty GTIN. The result: @firstbase came out empty and all NONPHARMA articles from the firstbase feed were silently missing, so oddb_article.xml shipped only the ~17k Refdata/Swissmedic base instead of the ~190k GS1 set. Now read with encoding "bom|utf-8", which strips the BOM (live file: all 192'807 rows parse).
|
|
3
6
|
|
data/README.md
CHANGED
|
@@ -56,7 +56,7 @@ see `--help`.
|
|
|
56
56
|
oddb2xml [option]
|
|
57
57
|
produced files are found under data
|
|
58
58
|
-a, --append Additional target nonpharma
|
|
59
|
-
-r, --artikelstamm Create Artikelstamm Version
|
|
59
|
+
-r, --artikelstamm Create Artikelstamm Version 6 for Elexis >= 3.1
|
|
60
60
|
-c, --compress-ext=<s> format F. {tar.gz|zip}
|
|
61
61
|
-e, --extended pharma, non-pharma plus prices and non-pharma from zurrose.
|
|
62
62
|
Products without EAN-Code will also be listed.
|
|
@@ -337,6 +337,40 @@ drugs from **2026-07-01**; from **2027-01-01** insurers may reject
|
|
|
337
337
|
invoices without it. See issue
|
|
338
338
|
[#113](https://github.com/zdavatz/oddb2xml/issues/113).
|
|
339
339
|
|
|
340
|
+
Since 3.0.26 the same codes are also carried per article in the
|
|
341
|
+
**Elexis Artikelstamm v6** output (`--artikelstamm`). Each SL
|
|
342
|
+
price-model `<ITEM>` gains an `<ARTSL>` block with one `<ARTLIM>` per
|
|
343
|
+
limitation:
|
|
344
|
+
|
|
345
|
+
```xml
|
|
346
|
+
<ITEM PHARMATYPE="P">
|
|
347
|
+
<GTIN>7680543780251</GTIN>
|
|
348
|
+
...
|
|
349
|
+
<PRODNO>58398</PRODNO>
|
|
350
|
+
<ARTSL>
|
|
351
|
+
<PM>true</PM>
|
|
352
|
+
<ARTLIMS>
|
|
353
|
+
<ARTLIM>
|
|
354
|
+
<LIMCD>MABTHERA.01</LIMCD> <!-- BAG limitation code -->
|
|
355
|
+
<INDCD>17079.01</INDCD> <!-- Indikationscode XXXXX.NN -->
|
|
356
|
+
<VDAT>2023-05-01T00:00:00</VDAT>
|
|
357
|
+
</ARTLIM>
|
|
358
|
+
<ARTLIM>
|
|
359
|
+
<LIMCD>MABTHERA.04</LIMCD>
|
|
360
|
+
<INDCD>17079.03</INDCD>
|
|
361
|
+
<VDAT>2026-05-01T00:00:00</VDAT>
|
|
362
|
+
<VTDAT>2026-06-30T00:00:00</VTDAT>
|
|
363
|
+
</ARTLIM>
|
|
364
|
+
</ARTLIMS>
|
|
365
|
+
</ARTSL>
|
|
366
|
+
</ITEM>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The output bumps from Artikelstamm v5 to **v6** (namespace
|
|
370
|
+
`http://elexis.ch/Elexis_Artikelstamm_v6`, file
|
|
371
|
+
`artikelstamm_DDMMYYYY_v6.xml`, validated against the bundled
|
|
372
|
+
`Elexis_Artikelstamm_v6.xsd`); consumers must switch to the v6 file.
|
|
373
|
+
|
|
340
374
|
## Limitation texts in `--fhir` mode
|
|
341
375
|
|
|
342
376
|
In 3.0.8 we fixed empty `<DescriptionDe/Fr/It>` on every `<Limitation>`
|
|
@@ -432,6 +466,29 @@ You can also run
|
|
|
432
466
|
for your currently open Terminal to download and set the Certificate.
|
|
433
467
|
|
|
434
468
|
|
|
469
|
+
## Troubleshooting downloads
|
|
470
|
+
|
|
471
|
+
oddb2xml runs a connectivity precheck and prints an `oddb2xml CONNECTIVITY
|
|
472
|
+
WARNING` listing any source host it cannot reach; set
|
|
473
|
+
`ODDB2XML_SKIP_PROXY_CHECK=1` to silence it.
|
|
474
|
+
|
|
475
|
+
A source host may finish the TLS handshake and then **reset the connection
|
|
476
|
+
before sending any HTTP response** (`Errno::ECONNRESET` / "Connection reset by
|
|
477
|
+
peer"; `wget` reports "Read error (Error in the pull function.) in headers").
|
|
478
|
+
When this happens for only one host while everything else is reachable, and a
|
|
479
|
+
normal browser still opens the site, it is usually an **anti-bot gateway (WAF)
|
|
480
|
+
that blocks by TLS fingerprint or source-IP reputation** rather than an outage
|
|
481
|
+
— a User-Agent change does not help. Mitigations: retry later from the same
|
|
482
|
+
host, run the download from a different egress IP, fetch the affected file with
|
|
483
|
+
a browser-grade client (a TLS-impersonating downloader or a headless browser)
|
|
484
|
+
into `downloads/` and continue with `--skip-download`, or ask the source to
|
|
485
|
+
allow-list the host.
|
|
486
|
+
|
|
487
|
+
The deployment driver `scripts/run_oddb2xml.sh` already retries the whole build
|
|
488
|
+
a few times on such transient failures (`ODDB2XML_RETRIES`,
|
|
489
|
+
`ODDB2XML_RETRY_DELAY`); a persistent per-host block still needs one of the
|
|
490
|
+
mitigations above.
|
|
491
|
+
|
|
435
492
|
## Testing
|
|
436
493
|
|
|
437
494
|
* Calling rake spec runs spec tests.
|
data/lib/oddb2xml/builder.rb
CHANGED
|
@@ -1562,9 +1562,47 @@ module Oddb2xml
|
|
|
1562
1562
|
end
|
|
1563
1563
|
end
|
|
1564
1564
|
|
|
1565
|
+
# Normalise a BAG validity date to an xs:dateTime for <VDAT>/<VTDAT>.
|
|
1566
|
+
# A bare date (YYYY-MM-DD) gets midnight appended (unzoned, still a valid
|
|
1567
|
+
# xs:dateTime); a full timestamp is passed through; blank -> nil.
|
|
1568
|
+
def elexis_datetime(str)
|
|
1569
|
+
s = str.to_s.strip
|
|
1570
|
+
return nil if s.empty?
|
|
1571
|
+
s = "#{s}T00:00:00" if /\A\d{4}-\d{2}-\d{2}\z/.match?(s)
|
|
1572
|
+
s
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
# Emit the v6 <ARTSL> block for one ITEM: one <ARTLIM> per limitation that
|
|
1576
|
+
# carries a BAG Indikationscode (INDCD). Mandatory on prescriptions/invoices
|
|
1577
|
+
# for SL price-model drugs from 2026-07-01 (issue #113). <PM> is "true"
|
|
1578
|
+
# because the BAG indication code is required only for price-model SL drugs,
|
|
1579
|
+
# which is exactly the set of items that reach this block.
|
|
1580
|
+
def append_artsl(xml, limitations)
|
|
1581
|
+
artlims = (limitations || []).select { |lim| lim[:indcd] && !lim[:indcd].to_s.empty? }
|
|
1582
|
+
artlims = artlims.uniq { |lim| [lim[:code], lim[:indcd]] }
|
|
1583
|
+
return if artlims.empty?
|
|
1584
|
+
xml.ARTSL do
|
|
1585
|
+
xml.PM "true"
|
|
1586
|
+
xml.ARTLIMS do
|
|
1587
|
+
artlims.each do |lim|
|
|
1588
|
+
xml.ARTLIM do
|
|
1589
|
+
xml.LIMCD lim[:code].to_s
|
|
1590
|
+
xml.INDCD lim[:indcd].to_s
|
|
1591
|
+
if (vdat = elexis_datetime(lim[:vdate]))
|
|
1592
|
+
xml.VDAT vdat
|
|
1593
|
+
end
|
|
1594
|
+
if (vtdat = elexis_datetime(lim[:vtdate]))
|
|
1595
|
+
xml.VTDAT vtdat
|
|
1596
|
+
end
|
|
1597
|
+
end
|
|
1598
|
+
end
|
|
1599
|
+
end
|
|
1600
|
+
end
|
|
1601
|
+
end
|
|
1602
|
+
|
|
1565
1603
|
def build_artikelstamm
|
|
1566
|
-
@@
|
|
1567
|
-
@csv_file = CSV.open(File.join(WORK_DIR, "artikelstamm_#{Date.today.strftime("%d%m%Y")}
|
|
1604
|
+
@@emitted_v6_gtins = []
|
|
1605
|
+
@csv_file = CSV.open(File.join(WORK_DIR, "artikelstamm_#{Date.today.strftime("%d%m%Y")}_v6.csv"), "w+")
|
|
1568
1606
|
@csv_file << ["gtin", "name", "pkg_size", "galenic_form", "price_ex_factory", "price_public", "prodno", "atc_code", "active_substance", "original", "it-code", "sl-liste"]
|
|
1569
1607
|
@csv_file.sync = true
|
|
1570
1608
|
variant = "build_artikelstamm"
|
|
@@ -1649,10 +1687,10 @@ module Oddb2xml
|
|
|
1649
1687
|
end
|
|
1650
1688
|
end
|
|
1651
1689
|
info = @calc_items[pkg_gtin]
|
|
1652
|
-
if @@
|
|
1690
|
+
if @@emitted_v6_gtins.index(pkg_gtin)
|
|
1653
1691
|
next
|
|
1654
1692
|
else
|
|
1655
|
-
@@
|
|
1693
|
+
@@emitted_v6_gtins << pkg_gtin.clone
|
|
1656
1694
|
end
|
|
1657
1695
|
options = {"PHARMATYPE" => "P"}
|
|
1658
1696
|
xml.ITEM(options) do
|
|
@@ -1771,6 +1809,8 @@ module Oddb2xml
|
|
|
1771
1809
|
end
|
|
1772
1810
|
end
|
|
1773
1811
|
xml.PRODNO prodno if prodno
|
|
1812
|
+
# v6 <ARTSL>: per-article BAG Indikationscodes (issue #113).
|
|
1813
|
+
append_artsl(xml, package[:limitations])
|
|
1774
1814
|
@csv_file << [pkg_gtin, name, package[:unit], measure,
|
|
1775
1815
|
pexf || "",
|
|
1776
1816
|
ppub || "",
|
|
@@ -1780,10 +1820,10 @@ module Oddb2xml
|
|
|
1780
1820
|
end
|
|
1781
1821
|
end
|
|
1782
1822
|
else # non pharma
|
|
1783
|
-
if @@
|
|
1823
|
+
if @@emitted_v6_gtins.index(ean13)
|
|
1784
1824
|
next
|
|
1785
1825
|
else
|
|
1786
|
-
@@
|
|
1826
|
+
@@emitted_v6_gtins << ean13.clone
|
|
1787
1827
|
end
|
|
1788
1828
|
# Set the pharmatype to 'Y' for outdated products, which are no longer found
|
|
1789
1829
|
# in refdata/packungen
|
|
@@ -1896,7 +1936,7 @@ module Oddb2xml
|
|
|
1896
1936
|
elexis_strftime_format = "%FT%T\.%L%:z"
|
|
1897
1937
|
@@cumul_ver = (Date.today.year - 2013) * 12 + Date.today.month
|
|
1898
1938
|
options_xml = {
|
|
1899
|
-
"xmlns" => "http://elexis.ch/
|
|
1939
|
+
"xmlns" => "http://elexis.ch/Elexis_Artikelstamm_v6",
|
|
1900
1940
|
"CREATION_DATETIME" => Time.new.strftime(elexis_strftime_format),
|
|
1901
1941
|
"BUILD_DATETIME" => Time.new.strftime(elexis_strftime_format),
|
|
1902
1942
|
"DATA_SOURCE" => "oddb2xml"
|
|
@@ -2008,7 +2048,7 @@ module Oddb2xml
|
|
|
2008
2048
|
end
|
|
2009
2049
|
end
|
|
2010
2050
|
end
|
|
2011
|
-
Oddb2xml.log "#{variant}. Done #{nr_products} of #{@products.size} products, #{@limitations.size} limitations and #{nr_articles}/#{@nr_articles} articles. @@
|
|
2051
|
+
Oddb2xml.log "#{variant}. Done #{nr_products} of #{@products.size} products, #{@limitations.size} limitations and #{nr_articles}/#{@nr_articles} articles. @@emitted_v6_gtins #{@@emitted_v6_gtins.size}"
|
|
2012
2052
|
# we don't add a SHA256 hash for each element in the article
|
|
2013
2053
|
# Oddb2xml.add_hash(a_builder.to_xml)
|
|
2014
2054
|
# doc = REXML::Document.new( source, { :raw => :all })
|
|
@@ -2018,13 +2058,13 @@ module Oddb2xml
|
|
|
2018
2058
|
lines << " - #{sprintf("%5d", @limitations.size)} limitations"
|
|
2019
2059
|
lines << " - #{sprintf("%5d", @nr_articles)} articles"
|
|
2020
2060
|
lines << " - #{sprintf("%5d", @@gtin2ignore.size)} ignored GTINS"
|
|
2021
|
-
@@
|
|
2061
|
+
@@articlestamm_v6_info_lines = lines
|
|
2022
2062
|
a_builder.to_xml({indent: 4, encoding: "UTF-8"})
|
|
2023
2063
|
end
|
|
2024
2064
|
|
|
2025
2065
|
private_class_method
|
|
2026
|
-
def self.
|
|
2027
|
-
@@
|
|
2066
|
+
def self.articlestamm_v6_info_lines
|
|
2067
|
+
@@articlestamm_v6_info_lines
|
|
2028
2068
|
end
|
|
2029
2069
|
end
|
|
2030
2070
|
end
|
data/lib/oddb2xml/cli.rb
CHANGED
|
@@ -103,8 +103,8 @@ module Oddb2xml
|
|
|
103
103
|
end
|
|
104
104
|
build
|
|
105
105
|
if @options[:artikelstamm] && system("which xmllint")
|
|
106
|
-
|
|
107
|
-
cmd = "xmllint --noout --schema #{
|
|
106
|
+
elexis_v6_xsd = File.expand_path(File.join(__FILE__, "..", "..", "..", "Elexis_Artikelstamm_v6.xsd"))
|
|
107
|
+
cmd = "xmllint --noout --schema #{elexis_v6_xsd} #{@the_files[:artikelstamm]}"
|
|
108
108
|
if system(cmd)
|
|
109
109
|
puts "Validatied #{@the_files[:artikelstamm]}"
|
|
110
110
|
else
|
|
@@ -399,7 +399,7 @@ module Oddb2xml
|
|
|
399
399
|
@the_files[:calc] = "oddb_calc.xml"
|
|
400
400
|
end
|
|
401
401
|
if @options[:artikelstamm]
|
|
402
|
-
@the_files[:artikelstamm] = "artikelstamm_#{Date.today.strftime("%d%m%Y")}
|
|
402
|
+
@the_files[:artikelstamm] = "artikelstamm_#{Date.today.strftime("%d%m%Y")}_v6.xml"
|
|
403
403
|
elsif @options[:address]
|
|
404
404
|
@the_files[:company] = "#{prefix}_betrieb.xml"
|
|
405
405
|
@the_files[:person] = "#{prefix}_medizinalperson.xml"
|
|
@@ -438,7 +438,7 @@ module Oddb2xml
|
|
|
438
438
|
end
|
|
439
439
|
if @options[:artikelstamm]
|
|
440
440
|
lines << "Generated artikelstamm.xml for Elexis"
|
|
441
|
-
lines += Builder.
|
|
441
|
+
lines += Builder.articlestamm_v6_info_lines
|
|
442
442
|
elsif @options[:address]
|
|
443
443
|
{
|
|
444
444
|
"Betrieb" => :@companies,
|
|
@@ -586,6 +586,10 @@ module Oddb2xml
|
|
|
586
586
|
limitation.LimitationNiveau = "" # Not in FHIR
|
|
587
587
|
limitation.LimitationValue = "" # Not in FHIR
|
|
588
588
|
limitation.LimitationCudRef = cud_ref # carried through for FR/IT resolution
|
|
589
|
+
# BAG Indikationscode (XXXXX.NN): the v6 Artikelstamm <ARTSL>/<ARTLIM>
|
|
590
|
+
# block carries it per article (issue #113). Independent of the CUD id
|
|
591
|
+
# (= LimitationCode), so read the explicit indicationCode extension.
|
|
592
|
+
limitation.IndicationCode = lim[:indication_code] || ""
|
|
589
593
|
limitation.DescriptionDe = text_de
|
|
590
594
|
limitation.DescriptionFr = "" # filled by merge_language from FR bundle
|
|
591
595
|
limitation.DescriptionIt = "" # filled by merge_language from IT bundle
|
|
@@ -819,7 +823,9 @@ module Oddb2xml
|
|
|
819
823
|
desc_fr: lim.DescriptionFr || "",
|
|
820
824
|
desc_it: lim.DescriptionIt || "",
|
|
821
825
|
cud_ref: lim.LimitationCudRef,
|
|
826
|
+
indcd: lim.IndicationCode || "",
|
|
822
827
|
vdate: lim.ValidFromDate || "",
|
|
828
|
+
vtdate: lim.ValidThruDate || "",
|
|
823
829
|
del: is_deleted
|
|
824
830
|
}
|
|
825
831
|
end
|
data/lib/oddb2xml/options.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Oddb2xml
|
|
|
17
17
|
produced files are found under data
|
|
18
18
|
EOS
|
|
19
19
|
opt :append, "Additional target nonpharma", default: false
|
|
20
|
-
opt :artikelstamm, "Create Artikelstamm Version
|
|
20
|
+
opt :artikelstamm, "Create Artikelstamm Version 6 for Elexis >= 3.1"
|
|
21
21
|
opt :compress_ext, "format F. {tar.gz|zip}", type: :string, default: nil, short: "c"
|
|
22
22
|
opt :extended, "pharma, non-pharma plus prices and non-pharma from zurrose.
|
|
23
23
|
Products without EAN-Code will also be listed.
|
data/lib/oddb2xml/version.rb
CHANGED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Generate the mediupdatexml.oddb.org landing page ($DOCROOT/index.html) with
|
|
4
|
+
# live product counts:
|
|
5
|
+
# PHARMA = Swissmedic-registered medicines (<SMNO> in default/oddb_article.xml)
|
|
6
|
+
# NONPHARMA = GTINs in the GS1 firstbase download (firstbase.csv, minus header)
|
|
7
|
+
#
|
|
8
|
+
# Usage: generate_index_html.sh DOCROOT [FIRSTBASE_CSV]
|
|
9
|
+
# DOCROOT where to write index.html (default /home/zdavatz/oddb2xml)
|
|
10
|
+
# FIRSTBASE_CSV the GS1 firstbase CSV (default <DOCROOT>-build/downloads/firstbase.csv)
|
|
11
|
+
#
|
|
12
|
+
# Called from run_oddb2xml.sh (after each build) and from
|
|
13
|
+
# setup_mediupdatexml_web.sh (initial creation). Counts that can't be computed
|
|
14
|
+
# are shown as "—".
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
DOCROOT="${1:-/home/zdavatz/oddb2xml}"
|
|
20
|
+
FIRSTBASE_CSV="${2:-${DOCROOT%/}-build/downloads/firstbase.csv}"
|
|
21
|
+
ARTICLE_XML="${DOCROOT%/}/default/oddb_article.xml"
|
|
22
|
+
|
|
23
|
+
# Apache combined access log(s) for the visitors/region graph (last STATS_DAYS).
|
|
24
|
+
# Readable by the user only if it is in the "adm" group (sudo usermod -aG adm <user>).
|
|
25
|
+
ACCESS_LOG_GLOB="${ACCESS_LOG_GLOB:-/var/log/apache2/mediupdatexml.oddb.org_access.log*}"
|
|
26
|
+
STATS_DAYS="${STATS_DAYS:-14}"
|
|
27
|
+
|
|
28
|
+
# Swiss-style thousands separator (192807 -> 192'807); "—" passes through.
|
|
29
|
+
group() { [[ "$1" =~ ^[0-9]+$ ]] && printf "%s" "$1" | sed -re ":a;s/([0-9])([0-9]{3})($|[^0-9])/\1'\2\3/;ta" || printf "%s" "$1"; }
|
|
30
|
+
|
|
31
|
+
total="—"
|
|
32
|
+
[[ -f "$ARTICLE_XML" ]] && total=$(grep -c '<ART ' "$ARTICLE_XML" || true)
|
|
33
|
+
|
|
34
|
+
pharma="—"
|
|
35
|
+
[[ -f "$ARTICLE_XML" ]] && pharma=$(grep -c '<SMNO>' "$ARTICLE_XML" || true)
|
|
36
|
+
|
|
37
|
+
nonpharma="—"
|
|
38
|
+
[[ -f "$FIRSTBASE_CSV" ]] && nonpharma=$(( $(wc -l < "$FIRSTBASE_CSV") - 1 ))
|
|
39
|
+
|
|
40
|
+
stand=$(date '+%d.%m.%Y %H:%M')
|
|
41
|
+
|
|
42
|
+
# Visitors/sessions/region graph as a ready-to-embed inline-SVG HTML fragment.
|
|
43
|
+
# Self-contained (pure Python stdlib + cached DB-IP country CSV). Stays empty
|
|
44
|
+
# when the access log is unreadable or has no data, so the page degrades to
|
|
45
|
+
# simply omitting the section.
|
|
46
|
+
stats_fragment=""
|
|
47
|
+
if [[ -f "$SCRIPT_DIR/visitor_stats.py" ]]; then
|
|
48
|
+
stats_fragment=$(python3 "$SCRIPT_DIR/visitor_stats.py" \
|
|
49
|
+
"$ACCESS_LOG_GLOB" "$(dirname "$FIRSTBASE_CSV")" "$STATS_DAYS" 2>/dev/null || true)
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
mkdir -p "$DOCROOT"
|
|
53
|
+
|
|
54
|
+
# Logo (self-contained SVG): brand-blue rounded badge, white pharma/Swiss cross
|
|
55
|
+
# flanked by XML angle brackets "< >". Written atomically next to index.html and
|
|
56
|
+
# used both as the top-right header image and as the favicon.
|
|
57
|
+
logo_tmp="${DOCROOT%/}/.logo.svg.$$"
|
|
58
|
+
cat > "$logo_tmp" <<'SVG'
|
|
59
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="oddb2xml">
|
|
60
|
+
<defs>
|
|
61
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
62
|
+
<stop offset="0" stop-color="#1a6dff"/>
|
|
63
|
+
<stop offset="1" stop-color="#0a3d8f"/>
|
|
64
|
+
</linearGradient>
|
|
65
|
+
</defs>
|
|
66
|
+
<rect width="64" height="64" rx="14" fill="url(#g)"/>
|
|
67
|
+
<g stroke="#e2231a" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
|
68
|
+
<polyline points="15,24 9,32 15,40"/>
|
|
69
|
+
<polyline points="49,24 55,32 49,40"/>
|
|
70
|
+
</g>
|
|
71
|
+
<g fill="#ffffff">
|
|
72
|
+
<rect x="26.5" y="18" width="11" height="28" rx="2.5"/>
|
|
73
|
+
<rect x="18" y="26.5" width="28" height="11" rx="2.5"/>
|
|
74
|
+
</g>
|
|
75
|
+
</svg>
|
|
76
|
+
SVG
|
|
77
|
+
chmod 644 "$logo_tmp"
|
|
78
|
+
mv -f "$logo_tmp" "${DOCROOT%/}/logo.svg"
|
|
79
|
+
|
|
80
|
+
# Write atomically via temp + mv so the page can be refreshed regardless of who
|
|
81
|
+
# owns the existing index.html (setup runs as root, run_oddb2xml.sh as the user);
|
|
82
|
+
# mv only needs write on the directory, which both have.
|
|
83
|
+
tmp="${DOCROOT%/}/.index.html.$$"
|
|
84
|
+
cat > "$tmp" <<HTML
|
|
85
|
+
<!DOCTYPE html>
|
|
86
|
+
<html lang="de">
|
|
87
|
+
<head>
|
|
88
|
+
<meta charset="utf-8">
|
|
89
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
90
|
+
<title>mediupdatexml.oddb.org — Downloads</title>
|
|
91
|
+
<!-- open every link in a new tab -->
|
|
92
|
+
<base target="_blank">
|
|
93
|
+
<meta name="referrer" content="strict-origin-when-cross-origin">
|
|
94
|
+
<link rel="icon" type="image/svg+xml" href="logo.svg">
|
|
95
|
+
<style>
|
|
96
|
+
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
97
|
+
max-width: 820px; margin: 2.5rem auto; padding: 0 1.2rem; color: #1a1a1a; line-height: 1.5; }
|
|
98
|
+
h1 { font-size: 1.6rem; margin-bottom: .2rem; }
|
|
99
|
+
h2 { font-size: 1.15rem; margin-top: 2rem; border-bottom: 1px solid #ddd; padding-bottom: .3rem; }
|
|
100
|
+
.sub { color: #666; margin-top: 0; }
|
|
101
|
+
ul { list-style: none; padding-left: 0; }
|
|
102
|
+
li { margin: .4rem 0; }
|
|
103
|
+
a { color: #0a58ca; text-decoration: none; }
|
|
104
|
+
a:hover { text-decoration: underline; }
|
|
105
|
+
.desc { color: #666; font-size: .9rem; }
|
|
106
|
+
code { background: #f4f4f4; padding: .1rem .3rem; border-radius: 3px; }
|
|
107
|
+
.stats { display: flex; gap: 1.5rem; margin: 1rem 0; flex-wrap: wrap; }
|
|
108
|
+
.stat { background: #f4f7fb; border: 1px solid #dce4ef; border-radius: 8px; padding: .8rem 1.2rem; }
|
|
109
|
+
.stat .n { font-size: 1.5rem; font-weight: 600; color: #0a58ca; }
|
|
110
|
+
.stat .l { color: #555; font-size: .85rem; }
|
|
111
|
+
footer { margin-top: 3rem; color: #888; font-size: .85rem; }
|
|
112
|
+
ul.firms { columns: 2; column-gap: 2rem; }
|
|
113
|
+
ul.firms li { margin: .25rem 0; break-inside: avoid; }
|
|
114
|
+
.topbar { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; }
|
|
115
|
+
.topbar .title h1 { margin: 0 0 .2rem; }
|
|
116
|
+
.topbar .logo { width: 64px; height: 64px; flex: 0 0 auto; }
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<header class="topbar">
|
|
121
|
+
<div class="title">
|
|
122
|
+
<h1>oddb2xml & aips2sqlite Downloads</h1>
|
|
123
|
+
<p class="sub">Schweizer Arzneimitteldaten — täglich aktualisiert (01:00 Uhr). Stand: ${stand}</p>
|
|
124
|
+
</div>
|
|
125
|
+
<a href="mailto:zdavatz@ywesee.com" title="Fragen? zdavatz at ywesee dot com"><img class="logo" src="logo.svg" alt="oddb2xml Logo" width="64" height="64"></a>
|
|
126
|
+
</header>
|
|
127
|
+
|
|
128
|
+
<div class="stats">
|
|
129
|
+
<div class="stat"><div class="n">$(group "$pharma")</div><div class="l">Medikamente (PHARMA)</div></div>
|
|
130
|
+
<div class="stat"><div class="n"><a href="https://id.gs1.ch/01/07612345000961">$(group "$nonpharma")</a></div><div class="l">Firstbase-Produkte (NONPHARMA)</div></div>
|
|
131
|
+
<div class="stat"><div class="n"><a href="default/oddb_article.xml">$(group "$total")</a></div><div class="l">Artikel total (<a href="default/oddb_article.xml"><code>oddb_article.xml</code></a>)</div></div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<h2>oddb2xml — Artikel-/Produkt-Feeds (<code>-b</code> firstbase)</h2>
|
|
135
|
+
<ul>
|
|
136
|
+
<li><a href="default/">default/</a> <span class="desc">— ohne Preisaufschlag</span></li>
|
|
137
|
+
<li><a href="45/">45/</a> <span class="desc">— Wiederverkaufspreis +45 %</span></li>
|
|
138
|
+
<li><a href="50/">50/</a> <span class="desc">— Wiederverkaufspreis +50 %</span></li>
|
|
139
|
+
<li><a href="55/">55/</a> <span class="desc">— Wiederverkaufspreis +55 %</span></li>
|
|
140
|
+
</ul>
|
|
141
|
+
<p class="desc">Jedes Verzeichnis enthält die gleichen Dateien (Direktlinks zum <code>default/</code>-Feed):
|
|
142
|
+
<a href="default/oddb_article.xml">oddb_article.xml</a>,
|
|
143
|
+
<a href="default/oddb_product.xml">oddb_product.xml</a>,
|
|
144
|
+
<a href="default/oddb_calc.xml">oddb_calc.xml</a>,
|
|
145
|
+
<a href="default/oddb_interaction.xml">oddb_interaction.xml</a>,
|
|
146
|
+
<a href="default/oddb_limitation.xml">oddb_limitation.xml</a>,
|
|
147
|
+
<a href="default/oddb_substance.xml">oddb_substance.xml</a>,
|
|
148
|
+
<a href="default/oddb_code.xml">oddb_code.xml</a> sowie
|
|
149
|
+
<a href="default/oddb2xml.zip">oddb2xml.zip</a> (alle Dateien gepackt).</p>
|
|
150
|
+
|
|
151
|
+
<h2>aips2sqlite — Fachinformationen & AmiKo-Datenbanken</h2>
|
|
152
|
+
<ul>
|
|
153
|
+
<li><a href="/aips2sqlite/fis/">fis/</a> <span class="desc">— Fachinformationen als XML/HTML (DE/FR/IT)</span></li>
|
|
154
|
+
<li><a href="/aips2sqlite/amiko_db_full_idx_de.db">amiko_db_full_idx_de.db</a> <span class="desc">— AmiKo-Datenbank Deutsch</span></li>
|
|
155
|
+
<li><a href="/aips2sqlite/amiko_db_full_idx_fr.db">amiko_db_full_idx_fr.db</a> <span class="desc">— AmiKo-Datenbank Französisch</span></li>
|
|
156
|
+
<li><a href="/aips2sqlite/oddb2xml_swissmedic_sequences.csv">oddb2xml_swissmedic_sequences.csv</a> <span class="desc">— Swissmedic-Sequenzen</span></li>
|
|
157
|
+
<li><a href="/aips2sqlite/atc_codes_used_set.txt">atc_codes_used_set.txt</a> <span class="desc">— verwendete ATC-Codes</span></li>
|
|
158
|
+
<li><a href="/aips2sqlite/">/aips2sqlite/</a> <span class="desc">— gesamtes Verzeichnis durchsuchen</span></li>
|
|
159
|
+
</ul>
|
|
160
|
+
|
|
161
|
+
<h2>MediUpdate XML bei HIN</h2>
|
|
162
|
+
<ul>
|
|
163
|
+
<li><a href="https://www.hin.ch/de/services/mediupdate-xml.cfm">www.hin.ch/de/services/mediupdate-xml.cfm</a></li>
|
|
164
|
+
</ul>
|
|
165
|
+
|
|
166
|
+
<h2>Softwarehäuser, die oddb2xml einsetzen</h2>
|
|
167
|
+
<ul class="firms">
|
|
168
|
+
<li><a href="https://www.advancedconcepts.ch/">Advanced Concepts AG</a></li>
|
|
169
|
+
<li><a href="https://www.bluecare.ch/">Bluecare AG</a></li>
|
|
170
|
+
<li><a href="https://corona.ch/">Corona Informatik AG</a></li>
|
|
171
|
+
<li><a href="https://www.derma2go.com/de/">derma2go AG</a></li>
|
|
172
|
+
<li><a href="https://www.diagnosia.com/">Diagnosia Internetservices GmbH</a></li>
|
|
173
|
+
<li><a href="https://elexis.ch/glp/index.html">Elexis</a></li>
|
|
174
|
+
<li><a href="https://www.emedswiss.ch/">emedSwiss SA</a></li>
|
|
175
|
+
<li><a href="https://gartenmann.ch/">Gartenmann Software AG</a></li>
|
|
176
|
+
<li><a href="https://hexabit.ch/">Hexabit GmbH</a></li>
|
|
177
|
+
<li><a href="https://www.hausarztmedizin.uzh.ch/de.html">Institut für Hausarztmedizin</a></li>
|
|
178
|
+
<li><a href="https://www.itw-informatik.ch/de/">ITW INFORMATIK AG</a></li>
|
|
179
|
+
<li><a href="https://www.lama-media.com/">Lama Media</a></li>
|
|
180
|
+
<li><a href="https://medab.org/country/ch">MEDAB</a></li>
|
|
181
|
+
<li><a href="https://www.pharmedsolutions.ch/">Pharmed Solutions GmbH</a></li>
|
|
182
|
+
<li><a href="https://praxinova.ch/">Praxinova AG</a></li>
|
|
183
|
+
<li><a href="https://seantis.ch/">seantis gmbh</a></li>
|
|
184
|
+
<li><a href="https://www.geteyesoft.ch/">Siplus SA – Eyesoft</a></li>
|
|
185
|
+
<li><a href="https://swiss-mr.ch/">SMR – Swiss Medical Record GmbH</a></li>
|
|
186
|
+
<li><a href="https://triboni.com/site/">Triboni AG</a></li>
|
|
187
|
+
<li><a href="https://www.vitabyte.ch/">Vitabyte AG</a></li>
|
|
188
|
+
<li><a href="https://zollsoft.de/">zollsoft GmbH</a></li>
|
|
189
|
+
</ul>
|
|
190
|
+
|
|
191
|
+
${stats_fragment}
|
|
192
|
+
|
|
193
|
+
<footer>
|
|
194
|
+
Fragen: <a href="mailto:zdavatz@ywesee.com">zdavatz at ywesee dot com</a> · Tel: 043 540 05 50
|
|
195
|
+
</footer>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
HTML
|
|
199
|
+
chmod 644 "$tmp"
|
|
200
|
+
mv -f "$tmp" "${DOCROOT%/}/index.html"
|
|
201
|
+
|
|
202
|
+
echo "Wrote ${DOCROOT%/}/index.html (PHARMA=$pharma NONPHARMA=$nonpharma)"
|
data/scripts/run_oddb2xml.sh
CHANGED
|
@@ -38,9 +38,35 @@ BUILD_DIR="${BUILD_DIR:-${OUT_DIR%/}-build}"
|
|
|
38
38
|
INCREMENTS="${INCREMENTS:-45 50 55}"
|
|
39
39
|
ODDB2XML_BIN="${ODDB2XML_BIN:-oddb2xml}"
|
|
40
40
|
TRANSFER_CMD="${TRANSFER_CMD:-$SCRIPT_DIR/transfer.sh}"
|
|
41
|
+
# Transient upstream download failures (e.g. Swissmedic resetting the
|
|
42
|
+
# connection, Errno::ECONNRESET) used to abort the whole nightly run under
|
|
43
|
+
# `set -e`. Retry the oddb2xml build a few times before giving up.
|
|
44
|
+
ODDB2XML_RETRIES="${ODDB2XML_RETRIES:-3}"
|
|
45
|
+
ODDB2XML_RETRY_DELAY="${ODDB2XML_RETRY_DELAY:-120}"
|
|
41
46
|
|
|
42
47
|
log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
|
|
43
48
|
|
|
49
|
+
# run_with_retry <description> -- <command...>
|
|
50
|
+
# Retry a flaky command up to ODDB2XML_RETRIES times, sleeping
|
|
51
|
+
# ODDB2XML_RETRY_DELAY seconds between attempts. Running the command as the
|
|
52
|
+
# `until` condition keeps it exempt from `set -e`, so a failed attempt retries
|
|
53
|
+
# instead of killing the script; the final failure is propagated via return.
|
|
54
|
+
run_with_retry() {
|
|
55
|
+
local desc="$1"; shift
|
|
56
|
+
[[ "${1:-}" == "--" ]] && shift
|
|
57
|
+
local attempt=1 rc=0
|
|
58
|
+
until "$@"; do
|
|
59
|
+
rc=$?
|
|
60
|
+
if [[ $attempt -ge $ODDB2XML_RETRIES ]]; then
|
|
61
|
+
log "ERROR: $desc failed after $attempt attempts (last exit $rc)"
|
|
62
|
+
return $rc
|
|
63
|
+
fi
|
|
64
|
+
log "WARNING: $desc failed (exit $rc), attempt $attempt/$ODDB2XML_RETRIES; retrying in ${ODDB2XML_RETRY_DELAY}s"
|
|
65
|
+
sleep "$ODDB2XML_RETRY_DELAY"
|
|
66
|
+
attempt=$((attempt + 1))
|
|
67
|
+
done
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
# 1. Install / update the published gem unless told otherwise.
|
|
45
71
|
if [[ "${SKIP_GEM_INSTALL:-0}" != "1" ]]; then
|
|
46
72
|
log "Installing oddb2xml gem"
|
|
@@ -69,7 +95,10 @@ build_one() {
|
|
|
69
95
|
|
|
70
96
|
log "Building increment '${inc:-none}' -> $dest"
|
|
71
97
|
rm -f oddb*.zip
|
|
72
|
-
|
|
98
|
+
# On a retry the first build re-downloads from scratch (dl_opt empty), which
|
|
99
|
+
# also clears any partial download left by the failed attempt.
|
|
100
|
+
run_with_retry "oddb2xml build '${inc:-none}'" -- \
|
|
101
|
+
"$ODDB2XML_BIN" "${dl_opt[@]}" -b "${inc_opt[@]}" -c zip
|
|
73
102
|
|
|
74
103
|
shopt -s nullglob
|
|
75
104
|
local zips=(oddb*.zip)
|
|
@@ -89,6 +118,14 @@ for inc in $INCREMENTS; do
|
|
|
89
118
|
done
|
|
90
119
|
build_one "" "default" # final run with no increment
|
|
91
120
|
|
|
121
|
+
# 2b. Refresh the download landing page with the live PHARMA/NONPHARMA counts
|
|
122
|
+
# (PHARMA from default/oddb_article.xml, NONPHARMA from the GS1 firstbase CSV).
|
|
123
|
+
if [[ -x "$SCRIPT_DIR/generate_index_html.sh" ]]; then
|
|
124
|
+
log "Refreshing landing page index.html"
|
|
125
|
+
"$SCRIPT_DIR/generate_index_html.sh" "$OUT_DIR" "$BUILD_DIR/downloads/firstbase.csv" || \
|
|
126
|
+
log "WARNING: could not regenerate index.html"
|
|
127
|
+
fi
|
|
128
|
+
|
|
92
129
|
# 3. Optional hand-off to the transfer step (scripts/transfer.sh).
|
|
93
130
|
if [[ "${RUN_TRANSFER:-0}" == "1" ]]; then
|
|
94
131
|
log "Running transfer: $TRANSFER_CMD"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Provision Apache to serve the oddb2xml output at https://mediupdatexml.oddb.org
|
|
4
|
+
# (Let's Encrypt HTTPS + browsable downloads, like pillbox.oddb.org).
|
|
5
|
+
#
|
|
6
|
+
# Serves two areas on one domain:
|
|
7
|
+
# / -> $DOCROOT (oddb2xml feeds: default/ 45/ 50/ 55/)
|
|
8
|
+
# /aips2sqlite/ -> $AIPS_OUT (FI XMLs, AmiKo .db, sequences CSV)
|
|
9
|
+
# plus a curated landing page ($DOCROOT/index.html) linking both.
|
|
10
|
+
#
|
|
11
|
+
# Run with: sudo scripts/setup_mediupdatexml_web.sh
|
|
12
|
+
#
|
|
13
|
+
# Idempotent: safe to re-run. HTTPS is issued/renewed automatically once the
|
|
14
|
+
# domain's DNS points at this host.
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
DOMAIN="mediupdatexml.oddb.org"
|
|
19
|
+
DOCROOT="/home/zdavatz/oddb2xml"
|
|
20
|
+
AIPS_OUT="/home/zdavatz/software/aips2sqlite/jars/output"
|
|
21
|
+
EMAIL="zdavatz@gmail.com"
|
|
22
|
+
SITE="/etc/apache2/sites-available/${DOMAIN}.conf"
|
|
23
|
+
|
|
24
|
+
if [[ $EUID -ne 0 ]]; then
|
|
25
|
+
echo "Please run as root: sudo $0" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
echo "==> Installing apache2 + certbot"
|
|
30
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
31
|
+
apt-get update -qq
|
|
32
|
+
apt-get install -y apache2 certbot python3-certbot-apache
|
|
33
|
+
|
|
34
|
+
echo "==> Enabling required Apache modules"
|
|
35
|
+
a2enmod autoindex headers >/dev/null
|
|
36
|
+
|
|
37
|
+
echo "==> Allowing www-data to traverse /home/zdavatz (711 = no listing, path access only)"
|
|
38
|
+
chmod 711 /home/zdavatz
|
|
39
|
+
|
|
40
|
+
echo "==> Writing vhost $SITE"
|
|
41
|
+
cat > "$SITE" <<EOF
|
|
42
|
+
<VirtualHost *:80>
|
|
43
|
+
ServerName ${DOMAIN}
|
|
44
|
+
DocumentRoot ${DOCROOT}
|
|
45
|
+
|
|
46
|
+
<Directory ${DOCROOT}>
|
|
47
|
+
Options +Indexes +FollowSymLinks
|
|
48
|
+
IndexOptions FancyIndexing HTMLTable NameWidth=* SuppressDescription FoldersFirst
|
|
49
|
+
Require all granted
|
|
50
|
+
AllowOverride None
|
|
51
|
+
</Directory>
|
|
52
|
+
|
|
53
|
+
ErrorLog \${APACHE_LOG_DIR}/${DOMAIN}_error.log
|
|
54
|
+
CustomLog \${APACHE_LOG_DIR}/${DOMAIN}_access.log combined
|
|
55
|
+
</VirtualHost>
|
|
56
|
+
EOF
|
|
57
|
+
|
|
58
|
+
echo "==> Writing aips2sqlite download alias (/aips2sqlite -> jars/output)"
|
|
59
|
+
cat > /etc/apache2/conf-available/mediupdatexml-aips.conf <<EOF
|
|
60
|
+
# aips2sqlite output (FI XMLs, AmiKo .db, swissmedic sequences CSV)
|
|
61
|
+
# Served on both the HTTP and HTTPS vhosts of ${DOMAIN}.
|
|
62
|
+
Alias /aips2sqlite ${AIPS_OUT}
|
|
63
|
+
<Directory ${AIPS_OUT}>
|
|
64
|
+
Options +Indexes +FollowSymLinks
|
|
65
|
+
IndexOptions FancyIndexing HTMLTable NameWidth=* SuppressDescription FoldersFirst
|
|
66
|
+
Require all granted
|
|
67
|
+
AllowOverride None
|
|
68
|
+
</Directory>
|
|
69
|
+
EOF
|
|
70
|
+
a2enconf mediupdatexml-aips >/dev/null
|
|
71
|
+
|
|
72
|
+
# Curated landing page (with live PHARMA/NONPHARMA counts). Lives in the OUT_DIR
|
|
73
|
+
# root, which run_oddb2xml.sh never touches (it only rebuilds the per-increment
|
|
74
|
+
# subdirs), so it survives rebuilds — and run_oddb2xml.sh refreshes the counts
|
|
75
|
+
# after every build via the same generator.
|
|
76
|
+
echo "==> Writing landing page ${DOCROOT}/index.html"
|
|
77
|
+
"$(dirname "$0")/generate_index_html.sh" "${DOCROOT}"
|
|
78
|
+
|
|
79
|
+
echo "==> Enabling site, disabling default"
|
|
80
|
+
a2ensite "${DOMAIN}.conf" >/dev/null
|
|
81
|
+
a2dissite 000-default.conf >/dev/null 2>&1 || true
|
|
82
|
+
|
|
83
|
+
echo "==> Testing config and reloading Apache"
|
|
84
|
+
apache2ctl configtest
|
|
85
|
+
systemctl reload apache2
|
|
86
|
+
systemctl enable apache2 >/dev/null 2>&1 || true
|
|
87
|
+
|
|
88
|
+
echo
|
|
89
|
+
echo "==> HTTP is live: http://${DOMAIN}/ (serving ${DOCROOT})"
|
|
90
|
+
echo
|
|
91
|
+
|
|
92
|
+
# --- HTTPS via Let's Encrypt -------------------------------------------------
|
|
93
|
+
# Check a public resolver (Cloudflare DoH), not the local one, which may still
|
|
94
|
+
# hold a negative cache. Let's Encrypt validates from its own resolvers anyway.
|
|
95
|
+
resolved=$(curl -s --max-time 8 \
|
|
96
|
+
"https://1.1.1.1/dns-query?name=${DOMAIN}&type=A" \
|
|
97
|
+
-H 'accept: application/dns-json' \
|
|
98
|
+
| python3 -c "import sys,json; a=json.load(sys.stdin).get('Answer',[]); print(a[0]['data'] if a else '')" 2>/dev/null || true)
|
|
99
|
+
if [[ -z "$resolved" ]]; then
|
|
100
|
+
echo "!! ${DOMAIN} does not resolve in public DNS yet."
|
|
101
|
+
echo " Once the A/AAAA records propagate, run:"
|
|
102
|
+
echo " sudo certbot --apache -d ${DOMAIN} --redirect -m ${EMAIL} --agree-tos -n"
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
echo "==> ${DOMAIN} resolves to ${resolved} — requesting Let's Encrypt cert"
|
|
107
|
+
certbot --apache -d "${DOMAIN}" --redirect -m "${EMAIL}" --agree-tos -n
|
|
108
|
+
echo
|
|
109
|
+
echo "==> Done. HTTPS is live: https://${DOMAIN}/"
|
|
110
|
+
echo " Certbot installed a systemd timer for auto-renewal."
|