v0.3.0; add med compliance module
This commit is contained in:
parent
6abccc5429
commit
eca61144e5
20 changed files with 1545 additions and 21 deletions
|
@ -8,6 +8,7 @@ WorkingDirectory=/var/lib/caddy/yoake
|
|||
ExecStart=/var/lib/caddy/yoake/server
|
||||
Restart=always
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
AppArmorProfile=yoake
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
42
etc/yoake.apparmor
Normal file
42
etc/yoake.apparmor
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Last Modified: Fri Nov 11 17:59:18 2022
|
||||
abi <abi/3.0>,
|
||||
|
||||
include <tunables/global>
|
||||
|
||||
@{YOAKE}="/var/lib/caddy/yoake"
|
||||
|
||||
@{YOAKE_DATA}="/var/lib/caddy/yoake.badger"
|
||||
|
||||
profile yoake @{YOAKE}/server {
|
||||
include <abstractions/base>
|
||||
include <abstractions/nameservice>
|
||||
include <abstractions/apparmor_api/introspect>
|
||||
^ssl {
|
||||
include <abstractions/base>
|
||||
include <abstractions/ssl_certs>
|
||||
include <abstractions/ssl_keys>
|
||||
include <abstractions/apparmor_api/introspect>
|
||||
}
|
||||
/sys/kernel/mm/transparent_hugepage/* r,
|
||||
/proc/sys/net/core/somaxconn r,
|
||||
@{YOAKE}/server mr,
|
||||
@{YOAKE}/** r,
|
||||
@{YOAKE_DATA}/ rwk,
|
||||
@{YOAKE_DATA}/** rwlk,
|
||||
capability net_bind_service,
|
||||
signal send peer=yoake//serve,
|
||||
|
||||
^serve flags=(kill) {
|
||||
include <abstractions/base>
|
||||
include <abstractions/nameservice>
|
||||
include <abstractions/apparmor_api/introspect>
|
||||
|
||||
/sys/kernel/mm/transparent_hugepage/ r,
|
||||
@{YOAKE}/server mr,
|
||||
@{YOAKE}/ r,
|
||||
@{YOAKE}/{assets,webroot}/** r,
|
||||
@{YOAKE_DATA}/ rwk,
|
||||
@{YOAKE_DATA}/** rwlk,
|
||||
signal receive peer=yoake,
|
||||
}
|
||||
}
|
11
go.mod
11
go.mod
|
@ -6,12 +6,16 @@ require (
|
|||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/dgraph-io/badger/v3 v3.2103.4
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.3
|
||||
github.com/eternal-flame-AD/yubigo v0.0.0-20221005082707-ce0c8989e8b1
|
||||
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/context v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/jinzhu/configor v1.2.1
|
||||
github.com/labstack/echo/v4 v4.9.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/twilio/twilio-go v1.1.1
|
||||
github.com/vanng822/go-premailer v1.20.1
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
|
@ -22,11 +26,9 @@ require (
|
|||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||
github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
|
@ -43,7 +45,7 @@ require (
|
|||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/stretchr/testify v1.8.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
github.com/vanng822/css v1.0.1 // indirect
|
||||
|
@ -56,4 +58,5 @@ require (
|
|||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
20
go.sum
20
go.sum
|
@ -3,6 +3,7 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
|
@ -31,16 +32,11 @@ github.com/dgraph-io/badger/v3 v3.2103.4 h1:WE1B07YNTTJTtG9xjBcSW2wn0RJLyiV99h95
|
|||
github.com/dgraph-io/badger/v3 v3.2103.4/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5 h1:BaeJtFDlto/NjX9t730OebRRJf2P+t9YEDz3ur18824=
|
||||
github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5/go.mod h1:Jcj7rFNlTknb18v9jpSA58BveX2LDhXqaoy+6YV1N9g=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.1 h1:LBWfkf/Mx0s6inwqurWC8nME7ICg4cDmh2fmOkBeenI=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.1/go.mod h1:K8VSDcvYN18uG+vsnR+3um4t6fX13Km6ci9mgQfDMg8=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.2 h1:sjDN6pyyjXBB+o+oDt6kyo2xiE8vvjZRABZ0fJEzHiE=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.2/go.mod h1:OpqESxf/LXsssooWBPzAoIAC2PtloCT1CmA+glQKYV8=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.3 h1:nFOxi6mbE8wpd5aHbSGvjbnaEjYC156IICWTteSgEIo=
|
||||
github.com/eternal-flame-AD/go-apparmor v0.0.3/go.mod h1:OpqESxf/LXsssooWBPzAoIAC2PtloCT1CmA+glQKYV8=
|
||||
github.com/eternal-flame-AD/yubigo v0.0.0-20221005082707-ce0c8989e8b1 h1:B+ad4UMWwNAUsZhLLQCCrEx+cfLsbf0+AbbcfG7RIv0=
|
||||
|
@ -52,7 +48,6 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
|
|||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
|
@ -60,7 +55,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
|||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
|
@ -73,7 +67,10 @@ github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6
|
|||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
|
@ -92,6 +89,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
|
@ -110,7 +108,6 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
|
|||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
@ -119,6 +116,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
|
@ -230,6 +228,7 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
@ -244,11 +243,10 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gG
|
|||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
|
|
@ -229,7 +229,7 @@ func registerYoutube(g *echo.Group, database db.DB) {
|
|||
defer txn.txn.Discard()
|
||||
|
||||
categories, err := txn.GetCategories()
|
||||
if err != nil {
|
||||
if err != nil && !db.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return c.JSON(http.StatusOK, categories)
|
||||
|
|
44
internal/health/api.go
Normal file
44
internal/health/api.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/auth"
|
||||
"github.com/eternal-flame-AD/yoake/internal/comm"
|
||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Register(g *echo.Group, db db.DB, comm *comm.Communicator) {
|
||||
megsG := g.Group("/meds")
|
||||
{
|
||||
shortHands := megsG.Group("/shorthand")
|
||||
{
|
||||
shortHands.GET("/parse", RESTParseShorthand())
|
||||
shortHands.POST("/parse", RESTParseShorthand())
|
||||
|
||||
shortHands.POST("/format", RESTFormatShorthand())
|
||||
}
|
||||
|
||||
writeMutex := new(sync.Mutex)
|
||||
directions := megsG.Group("/directions", auth.RequireMiddleware(auth.RoleAdmin))
|
||||
{
|
||||
directions.GET("", RESTMedGetDirections(db))
|
||||
directions.POST("", RESTMedPostDirections(db, writeMutex))
|
||||
directions.DELETE("/:name", RESTMedDeleteDirections(db, writeMutex))
|
||||
}
|
||||
|
||||
compliance := megsG.Group("/compliance", auth.RequireMiddleware(auth.RoleAdmin))
|
||||
{
|
||||
complianceByMed := compliance.Group("/med/:med")
|
||||
{
|
||||
complianceByMed.GET("/log", RESTComplianceLogGet(db))
|
||||
complianceByMed.GET("/project", RESTComplianceLogProjectMed(db))
|
||||
}
|
||||
|
||||
compliance.GET("/log", RESTComplianceLogGet(db))
|
||||
|
||||
compliance.POST("/log", RESTComplianceLogPost(db, writeMutex))
|
||||
}
|
||||
}
|
||||
}
|
126
internal/health/api_compliance.go
Normal file
126
internal/health/api_compliance.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||
"github.com/eternal-flame-AD/yoake/internal/util"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func RESTComplianceLogGet(database db.DB) func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
filterKeyname := c.Param("med")
|
||||
from := c.QueryParam("from")
|
||||
to := c.QueryParam("to")
|
||||
if to == "" {
|
||||
to = time.Now().Format("2006-01-02")
|
||||
}
|
||||
if from == "" {
|
||||
from = time.Now().AddDate(0, 0, -30).Format("2006-01-02")
|
||||
}
|
||||
fromTime, err := time.Parse("2006-01-02", from)
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(400, err)
|
||||
}
|
||||
toTime, err := time.Parse("2006-01-02", to)
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(400, err)
|
||||
}
|
||||
period := util.NewDateRange(fromTime, toTime)
|
||||
if days := period.Days(); days > 180 {
|
||||
return echoerror.NewHttp(400, fmt.Errorf("invalid date range: %v", period))
|
||||
}
|
||||
logs, err := DBMedComplianceLogGet(database, period)
|
||||
if db.IsNotFound(err) || logs == nil {
|
||||
return c.JSON(200, ComplianceLogList{})
|
||||
} else if err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
if filterKeyname != "" {
|
||||
filtered := make(ComplianceLogList, 0, len(logs))
|
||||
for _, log := range logs {
|
||||
if log.MedKeyname == filterKeyname {
|
||||
filtered = append(filtered, log)
|
||||
}
|
||||
}
|
||||
logs = filtered
|
||||
}
|
||||
|
||||
return c.JSON(200, logs)
|
||||
}
|
||||
}
|
||||
|
||||
func RESTComplianceLogPost(db db.DB, writeMutex *sync.Mutex) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var input ComplianceLog
|
||||
if err := c.Bind(&input); err != nil {
|
||||
return echoerror.NewHttp(400, err)
|
||||
}
|
||||
if input.Actual.Time.IsZero() {
|
||||
return echoerror.NewHttp(400, fmt.Errorf("invalid date"))
|
||||
}
|
||||
writeMutex.Lock()
|
||||
defer writeMutex.Unlock()
|
||||
|
||||
meds, err := DBMedListGet(db)
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
|
||||
var dir *Direction
|
||||
for _, med := range meds {
|
||||
d := med
|
||||
if med.KeyName() == input.MedKeyname {
|
||||
dir = &d
|
||||
} else if med.Name == input.MedKeyname {
|
||||
input.MedKeyname = med.KeyName()
|
||||
dir = &d
|
||||
}
|
||||
}
|
||||
if dir == nil {
|
||||
return echoerror.NewHttp(404, fmt.Errorf("med not found"))
|
||||
}
|
||||
|
||||
if err := DBMedComplianceLogSetOne(db, *dir, &input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if input.Actual.Dose <= 0 {
|
||||
return c.NoContent(204)
|
||||
}
|
||||
|
||||
return c.JSON(200, input)
|
||||
}
|
||||
}
|
||||
|
||||
func RESTComplianceLogProjectMed(db db.DB) func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
keyName := c.Param("med")
|
||||
meds, err := DBMedListGet(db)
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
|
||||
var dir *Direction
|
||||
for _, med := range meds {
|
||||
if med.KeyName() == keyName {
|
||||
d := med
|
||||
dir = &d
|
||||
}
|
||||
}
|
||||
if dir == nil {
|
||||
return echoerror.NewHttp(404, fmt.Errorf("med not found"))
|
||||
}
|
||||
|
||||
complianceLog, err := DBMedComplianceLogGet(db, util.DateRangeAround(time.Now(), 1))
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
|
||||
return c.JSON(200, complianceLog.ProjectNextDose(*dir))
|
||||
}
|
||||
}
|
92
internal/health/api_directions.go
Normal file
92
internal/health/api_directions.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func RESTMedGetDirections(db db.DB) func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.Error(echoerror.NewHttp(500, fmt.Errorf("internal error: %v", err)))
|
||||
}
|
||||
}()
|
||||
meds, err := DBMedListGet(db)
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
return c.JSON(200, meds)
|
||||
}
|
||||
}
|
||||
|
||||
func RESTMedPostDirections(db db.DB, writeMutex *sync.Mutex) func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
var input Direction
|
||||
if err := c.Bind(&input); err != nil {
|
||||
return echoerror.NewHttp(400, err)
|
||||
}
|
||||
if input.Name == "" {
|
||||
return echoerror.NewHttp(400, fmt.Errorf("name cannot be empty"))
|
||||
}
|
||||
if input.Dosage <= 0 {
|
||||
return echoerror.NewHttp(400, fmt.Errorf("dosage must be positive"))
|
||||
}
|
||||
if input.PeriodHours <= 0 {
|
||||
return echoerror.NewHttp(400, fmt.Errorf("period must be positive"))
|
||||
}
|
||||
writeMutex.Lock()
|
||||
defer writeMutex.Unlock()
|
||||
meds, err := DBMedListGet(db)
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
found := false
|
||||
for i, med := range meds {
|
||||
if med.KeyName() == input.KeyName() {
|
||||
meds[i] = input
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
meds = append(meds, input)
|
||||
}
|
||||
if err := DBMedListSet(db, meds); err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
return c.JSON(200, meds)
|
||||
}
|
||||
}
|
||||
|
||||
func RESTMedDeleteDirections(db db.DB, writeMutex *sync.Mutex) func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
writeMutex.Lock()
|
||||
defer writeMutex.Unlock()
|
||||
meds, err := DBMedListGet(db)
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
found := false
|
||||
for i, med := range meds {
|
||||
if strings.EqualFold(med.KeyName(), name) {
|
||||
meds = append(meds[:i], meds[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return echoerror.NewHttp(404, fmt.Errorf("med not found"))
|
||||
}
|
||||
if err := DBMedListSet(db, meds); err != nil {
|
||||
return echoerror.NewHttp(500, err)
|
||||
}
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
}
|
53
internal/health/api_shorthand.go
Normal file
53
internal/health/api_shorthand.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func RESTParseShorthand() func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.Error(echoerror.NewHttp(500, fmt.Errorf("internal error: %v", err)))
|
||||
}
|
||||
}()
|
||||
var inputStr string
|
||||
if c.Request().Method == "GET" {
|
||||
inputStr = c.QueryParam("shorthand")
|
||||
} else if c.Request().Method == "POST" {
|
||||
inputStr = c.FormValue("shorthand")
|
||||
} else {
|
||||
return echoerror.NewHttp(405, fmt.Errorf("unsupported method"))
|
||||
}
|
||||
inputStr = strings.TrimSpace(inputStr)
|
||||
parsed, err := ParseShorthand(inputStr)
|
||||
if err != nil {
|
||||
return echoerror.NewHttp(400, err)
|
||||
}
|
||||
return c.JSON(200, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
func RESTFormatShorthand() func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.Error(echoerror.NewHttp(500, fmt.Errorf("internal error: %v", err)))
|
||||
}
|
||||
}()
|
||||
var input Direction
|
||||
if err := c.Bind(&input); err != nil {
|
||||
return echoerror.NewHttp(400, err)
|
||||
}
|
||||
name, formatted := input.ShortHand()
|
||||
return c.JSON(200, map[string]string{
|
||||
"name": name,
|
||||
"direction": formatted,
|
||||
"__disclaimer": DirectionDisclaimer,
|
||||
})
|
||||
}
|
||||
}
|
152
internal/health/compliance.go
Normal file
152
internal/health/compliance.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/util"
|
||||
)
|
||||
|
||||
type ComplianceLog struct {
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
MedKeyname string `json:"med_keyname"`
|
||||
|
||||
Expected ComplianceDoseInfo `json:"expected"`
|
||||
Actual ComplianceDoseInfo `json:"actual"`
|
||||
|
||||
// 0 = closest to expected time +1 = closest to next expected dose
|
||||
// get a cumsum of this to get a compliance stat
|
||||
DoseOffset float64 `json:"dose_offset"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ComplianceDoseInfo struct {
|
||||
Time time.Time `json:"time"`
|
||||
Dose int `json:"dose"`
|
||||
}
|
||||
|
||||
type ComplianceLogList []ComplianceLog
|
||||
|
||||
func (c ComplianceLogList) ProjectNextDose(dir Direction) (nextDose ComplianceLog) {
|
||||
sort.Sort(c)
|
||||
|
||||
var lastDose ComplianceLog
|
||||
var cumDosage int
|
||||
for ptr := 0; ptr < len(c); ptr++ {
|
||||
if c[ptr].MedKeyname == dir.KeyName() {
|
||||
if dir.OptSchedule == OptScheduleWholeDose {
|
||||
lastDose = c[ptr]
|
||||
break
|
||||
} else {
|
||||
cumDosage += c[ptr].Actual.Dose
|
||||
if cumDosage < dir.Dosage {
|
||||
continue
|
||||
} else {
|
||||
lastDose = c[ptr]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if lastDose.UUID == "" /* not found */ {
|
||||
nextDose = ComplianceLog{
|
||||
MedKeyname: dir.KeyName(),
|
||||
Expected: ComplianceDoseInfo{
|
||||
Time: time.Now(),
|
||||
Dose: dir.Dosage,
|
||||
},
|
||||
Actual: ComplianceDoseInfo{
|
||||
Time: time.Now(),
|
||||
Dose: dir.Dosage,
|
||||
},
|
||||
DoseOffset: 0,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
} else {
|
||||
nextDose = ComplianceLog{
|
||||
MedKeyname: dir.KeyName(),
|
||||
Expected: ComplianceDoseInfo{
|
||||
Time: lastDose.Actual.Time.Add(time.Duration(dir.PeriodHours) * time.Hour),
|
||||
Dose: dir.Dosage,
|
||||
},
|
||||
Actual: ComplianceDoseInfo{
|
||||
Time: time.Now(),
|
||||
Dose: dir.Dosage,
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
nextDose.DoseOffset, _, _ = c.ComputeDoseOffset(dir, &nextDose)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c ComplianceLogList) ComputeDoseOffset(dir Direction, newLog *ComplianceLog) (float64, bool, error) {
|
||||
sort.Sort(c)
|
||||
|
||||
var lastTwoDoses []ComplianceLog
|
||||
if newLog != nil {
|
||||
lastTwoDoses = []ComplianceLog{*newLog}
|
||||
}
|
||||
for ptr := 0; len(lastTwoDoses) < 2 && ptr < len(c); ptr++ {
|
||||
if c[ptr].MedKeyname == dir.KeyName() {
|
||||
if len(lastTwoDoses) == 0 || lastTwoDoses[0].Actual.Time.After(c[ptr].Actual.Time) {
|
||||
lastTwoDoses = append(lastTwoDoses, c[ptr])
|
||||
}
|
||||
}
|
||||
}
|
||||
if newLog != nil {
|
||||
if newLog.Expected.Dose == 0 && dir.KeyName() == newLog.MedKeyname {
|
||||
newLog.Expected.Dose = dir.Dosage
|
||||
}
|
||||
if newLog.Expected.Time.IsZero() {
|
||||
if len(lastTwoDoses) == 2 {
|
||||
newLog.Expected.Time = lastTwoDoses[1].Actual.Time.Add(time.Duration(dir.PeriodHours) * time.Hour)
|
||||
} else {
|
||||
newLog.Expected.Time = newLog.Actual.Time
|
||||
}
|
||||
}
|
||||
lastTwoDoses[0] = *newLog
|
||||
}
|
||||
if len(lastTwoDoses) < 2 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
// now we have:
|
||||
// *exp ~actual
|
||||
// * ~ ~ *
|
||||
// offset = (new_actual - last_expected) / diff(new_expected, last_actual) - 1
|
||||
|
||||
if lastTwoDoses[0].Actual.Time.IsZero() {
|
||||
lastTwoDoses[0].Actual.Time = time.Now()
|
||||
}
|
||||
offset := float64(lastTwoDoses[0].Actual.Time.Sub(lastTwoDoses[1].Expected.Time))/
|
||||
float64(lastTwoDoses[0].Expected.Time.Sub(lastTwoDoses[1].Actual.Time)) - 1
|
||||
|
||||
// for prn ignore positive offsets
|
||||
if util.Contain(dir.Flags, DirectionFlagPRN) {
|
||||
if offset > 0 {
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
if math.Abs(offset) > 2 {
|
||||
// stop counting if three or more doses are missed
|
||||
return 0, false, nil
|
||||
}
|
||||
return offset, true, nil
|
||||
}
|
||||
|
||||
func (c ComplianceLogList) Len() int {
|
||||
return len(c)
|
||||
}
|
||||
|
||||
func (c ComplianceLogList) Less(i, j int) bool {
|
||||
return c[i].Actual.Time.After(c[j].Actual.Time)
|
||||
}
|
||||
|
||||
func (c ComplianceLogList) Swap(i, j int) {
|
||||
c[i], c[j] = c[j], c[i]
|
||||
}
|
164
internal/health/db.go
Normal file
164
internal/health/db.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||
"github.com/eternal-flame-AD/yoake/internal/util"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func DBMedListGet(database db.DB) ([]Direction, error) {
|
||||
txn := database.NewTransaction(false)
|
||||
defer txn.Discard()
|
||||
|
||||
var meds []Direction
|
||||
if err := db.GetJSON(txn, []byte("health_meds_list"), &meds); db.IsNotFound(err) {
|
||||
err = DBMedListSet(database, []Direction{})
|
||||
return meds, err
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return meds, nil
|
||||
}
|
||||
|
||||
func DBMedListSet(database db.DB, meds []Direction) error {
|
||||
txn := database.NewTransaction(true)
|
||||
defer txn.Discard()
|
||||
|
||||
for i, med := range meds {
|
||||
_, meds[i].DirectionShorthand = med.ShortHand()
|
||||
}
|
||||
if err := db.SetJSON(txn, []byte("health_meds_list"), meds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return txn.Commit()
|
||||
}
|
||||
|
||||
const dbMedComplianceLogPrefix = "health_meds_compliance_log_"
|
||||
|
||||
func DBMedComplianceLogGet(database db.DB, dates util.DateRange) (ComplianceLogList, error) {
|
||||
txn := database.NewTransaction(false)
|
||||
defer txn.Discard()
|
||||
|
||||
endIndex := dates.To.UTC().Format("2006-01")
|
||||
indexesToFetch := []string{endIndex}
|
||||
for indexNow := dates.From.UTC().AddDate(0, -1, 0); indexNow.Before(dates.To.AddDate(0, 1, 0)); indexNow = indexNow.AddDate(0, 1, 0) {
|
||||
indexesToFetch = append(indexesToFetch, indexNow.Format("2006-01"))
|
||||
}
|
||||
indexesToFetch = util.Unique(indexesToFetch)
|
||||
sort.Strings(indexesToFetch)
|
||||
|
||||
var res ComplianceLogList
|
||||
for _, index := range indexesToFetch {
|
||||
var log []ComplianceLog
|
||||
if err := db.GetJSON(txn, []byte(dbMedComplianceLogPrefix+index), &log); db.IsNotFound(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, log...)
|
||||
}
|
||||
sort.Sort(res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog) error {
|
||||
|
||||
index := log.Actual.Time.UTC().Format("2006-01")
|
||||
|
||||
existingLogs, err := DBMedComplianceLogGet(database, util.DateRangeAround(log.Actual.Time, 1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txn := database.NewTransaction(true)
|
||||
defer txn.Discard()
|
||||
|
||||
del := false
|
||||
if log.Actual.Dose == 0 {
|
||||
return echoerror.NewHttp(400, errors.New("dose cannot be zero"))
|
||||
} else if log.Actual.Dose < 0 {
|
||||
del = true
|
||||
}
|
||||
|
||||
if log.UUID != "" {
|
||||
foundIdx := -1
|
||||
for i, existingLog := range existingLogs {
|
||||
if existingLog.UUID == log.UUID {
|
||||
log.UpdatedAt = time.Now()
|
||||
foundIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundIdx < 0 {
|
||||
return echoerror.NewHttp(404, errors.New("log with specified UUID not found"))
|
||||
}
|
||||
origLog := existingLogs[foundIdx]
|
||||
log.CreatedAt = origLog.CreatedAt
|
||||
origLogIdx := origLog.Actual.Time.UTC().Format("2006-01")
|
||||
|
||||
if origLogIdx == index {
|
||||
// update and return
|
||||
if del {
|
||||
existingLogs = append(existingLogs[:foundIdx], existingLogs[foundIdx+1:]...)
|
||||
} else {
|
||||
log.UpdatedAt = time.Now()
|
||||
existingLogs[foundIdx] = *log
|
||||
}
|
||||
if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), existingLogs); err != nil {
|
||||
return err
|
||||
}
|
||||
return txn.Commit()
|
||||
} else {
|
||||
// delete from old index
|
||||
existingLogs = append(existingLogs[:foundIdx], existingLogs[foundIdx+1:]...)
|
||||
if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+origLogIdx), existingLogs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if del {
|
||||
return txn.Commit()
|
||||
}
|
||||
|
||||
if log.UUID == "" {
|
||||
log.UUID = uuid.New().String()
|
||||
log.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
log.UpdatedAt = time.Now()
|
||||
var logs ComplianceLogList
|
||||
if err := db.GetJSON(txn, []byte(dbMedComplianceLogPrefix+index), &logs); db.IsNotFound(err) {
|
||||
logs = []ComplianceLog{*log}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
logs = append(logs, *log)
|
||||
}
|
||||
for i := len(logs) - 1; i >= 0; i-- {
|
||||
offset, upd, err := logs.ComputeDoseOffset(dir, &logs[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if upd {
|
||||
logs[i].DoseOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(ComplianceLogList(logs))
|
||||
if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), logs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*log = logs[len(logs)-1]
|
||||
|
||||
return txn.Commit()
|
||||
|
||||
}
|
230
internal/health/parser.go
Normal file
230
internal/health/parser.go
Normal file
|
@ -0,0 +1,230 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/internal/util"
|
||||
)
|
||||
|
||||
type Direction struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
PeriodHours int `json:"period_hours"`
|
||||
|
||||
Dosage int `json:"dosage"`
|
||||
DosageUnit string `json:"dosage_unit"`
|
||||
DosageRoute string `json:"dosage_route"`
|
||||
|
||||
Flags []DirectionFlag `json:"flags"`
|
||||
|
||||
DirectionShorthand string `json:"shorthand"`
|
||||
|
||||
OptSchedule OptSchedule `json:"schedule"`
|
||||
|
||||
Disclaimer string `json:"__disclaimer"`
|
||||
}
|
||||
|
||||
const DirectionDisclaimer = "For personal use only. No warranty of accuracy."
|
||||
|
||||
type DirectionFlag string
|
||||
type OptSchedule string
|
||||
|
||||
const (
|
||||
DirectionFlagAM DirectionFlag = "qam"
|
||||
DirectionFlagHS DirectionFlag = "qhs"
|
||||
DirectionFlagPRN DirectionFlag = "prn"
|
||||
OptScheduleDefault OptSchedule = "default"
|
||||
OptScheduleWholeDose OptSchedule = "whole"
|
||||
)
|
||||
|
||||
func ParseShorthand(shorthand string) (*Direction, error) {
|
||||
res := new(Direction)
|
||||
res.Disclaimer = DirectionDisclaimer
|
||||
res.Flags = make([]DirectionFlag, 0)
|
||||
words := strings.Split(shorthand, " ")
|
||||
|
||||
optionsRegex := regexp.MustCompile(`^([a-zA-Z]+)\((\w+)\)$`)
|
||||
for i := len(words) - 1; i >= 0; i-- {
|
||||
if match := optionsRegex.FindStringSubmatch(words[i]); match != nil {
|
||||
name, value := match[1], match[2]
|
||||
switch strings.ToLower(name) {
|
||||
case "sched":
|
||||
fallthrough
|
||||
case "schedule":
|
||||
if res.OptSchedule != "" {
|
||||
return nil, fmt.Errorf("duplicate schedule option")
|
||||
}
|
||||
switch strings.ToLower(value) {
|
||||
case "default":
|
||||
fallthrough
|
||||
case "":
|
||||
res.OptSchedule = OptScheduleDefault
|
||||
case "whole":
|
||||
res.OptSchedule = OptScheduleWholeDose
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid schedule option: %s", value)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown option %s", name)
|
||||
}
|
||||
|
||||
words = words[:i]
|
||||
}
|
||||
}
|
||||
if res.OptSchedule == "" {
|
||||
res.OptSchedule = OptScheduleDefault
|
||||
}
|
||||
|
||||
// combined numbers and units
|
||||
for i := range words {
|
||||
digits := regexp.MustCompile(`^\d+$`)
|
||||
if digits.MatchString(words[i]) {
|
||||
words[i] = words[i] + words[i+1]
|
||||
words[i+1] = ""
|
||||
}
|
||||
}
|
||||
words = util.AntiJoin(words, []string{""})
|
||||
|
||||
// find prn keyword
|
||||
for i := len(words) - 1; i >= 0; i-- {
|
||||
if strings.EqualFold(words[i], "prn") {
|
||||
res.Flags = append(res.Flags, DirectionFlagPRN)
|
||||
words = append(words[:i], words[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
freqIdx := len(words) - 1
|
||||
if lastWord := strings.ToLower(words[len(words)-1]); lastWord == "bid" {
|
||||
res.PeriodHours = 12
|
||||
words = words[:len(words)-1]
|
||||
} else if lastWord == "tid" {
|
||||
res.PeriodHours = 8
|
||||
words = words[:len(words)-1]
|
||||
} else if lastWord == "qid" {
|
||||
res.PeriodHours = 6
|
||||
words = words[:len(words)-1]
|
||||
} else {
|
||||
for i := len(words) - 1; i >= 0; i-- {
|
||||
if strings.HasPrefix(strings.ToLower(words[i]), "q") {
|
||||
freqIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
freqStr := strings.ToLower(strings.Join(words[freqIdx:], ""))[1:]
|
||||
if freqStr == "am" {
|
||||
res.Flags = append(res.Flags, DirectionFlagAM)
|
||||
res.PeriodHours = 24
|
||||
} else if freqStr == "hs" {
|
||||
res.Flags = append(res.Flags, DirectionFlagHS)
|
||||
res.PeriodHours = 24
|
||||
} else {
|
||||
if !(freqStr[0] >= '0' && freqStr[0] <= '9') {
|
||||
freqStr = "1" + freqStr
|
||||
}
|
||||
freqRegexp := regexp.MustCompile(`^([0-9]+)([a-z]+)$`)
|
||||
freqMatch := freqRegexp.FindStringSubmatch(freqStr)
|
||||
if freqMatch == nil {
|
||||
return nil, fmt.Errorf("invalid frequency: %s", freqStr)
|
||||
}
|
||||
freq, err := strconv.Atoi(freqMatch[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid frequency number : %s", freqMatch[1])
|
||||
}
|
||||
if freqMatch[2] == "d" {
|
||||
res.PeriodHours = freq * 24
|
||||
} else if freqMatch[2] == "h" {
|
||||
res.PeriodHours = freq
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid frequency unit: %s", freqMatch[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
words = words[:freqIdx]
|
||||
|
||||
dosageRegexp := regexp.MustCompile(`^([0-9]+)([a-z]*)$`)
|
||||
var dosageMatch []string
|
||||
if dosageMatch = dosageRegexp.FindStringSubmatch(words[len(words)-1]); dosageMatch == nil {
|
||||
if dosageMatch = dosageRegexp.FindStringSubmatch(words[len(words)-2]); dosageMatch == nil {
|
||||
return nil, fmt.Errorf("invalid dosage: %s", words[len(words)-2:])
|
||||
} else {
|
||||
res.DosageRoute = words[len(words)-1]
|
||||
words = words[:len(words)-2]
|
||||
}
|
||||
} else {
|
||||
words = words[:len(words)-1]
|
||||
}
|
||||
dosage, err := strconv.Atoi(dosageMatch[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid dosage number: %s", dosageMatch[1])
|
||||
}
|
||||
res.Dosage = dosage
|
||||
res.DosageUnit = dosageMatch[2]
|
||||
|
||||
res.Name = strings.Join(words, " ")
|
||||
|
||||
s1, s2 := res.ShortHand()
|
||||
res.DirectionShorthand = s1 + " " + s2
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d Direction) KeyName() string {
|
||||
return strings.ToLower(strings.SplitN(d.Name, " ", 2)[0])
|
||||
}
|
||||
func (d *Direction) ShortHand() (name string, direction string) {
|
||||
builder := new(strings.Builder)
|
||||
builder.WriteString(strconv.Itoa(d.Dosage))
|
||||
if d.DosageUnit != "" {
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(d.DosageUnit)
|
||||
}
|
||||
if d.DosageRoute != "" {
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(d.DosageRoute)
|
||||
}
|
||||
if d.PeriodHours%24 == 0 {
|
||||
qNd := d.PeriodHours / 24
|
||||
qNdS := strconv.Itoa(qNd)
|
||||
if qNd == 1 {
|
||||
if util.Contain(d.Flags, DirectionFlagAM) {
|
||||
qNdS = "AM"
|
||||
} else if util.Contain(d.Flags, DirectionFlagHS) {
|
||||
qNdS = "HS"
|
||||
} else {
|
||||
qNdS = "d"
|
||||
}
|
||||
} else {
|
||||
qNdS += "d"
|
||||
}
|
||||
fmt.Fprintf(builder, " q%s", qNdS)
|
||||
} else if d.PeriodHours == 12 {
|
||||
builder.WriteString(" bid")
|
||||
} else if d.PeriodHours == 8 {
|
||||
builder.WriteString(" tid")
|
||||
} else if d.PeriodHours == 6 {
|
||||
builder.WriteString(" qid")
|
||||
} else {
|
||||
fmt.Fprintf(builder, " q%sh", strconv.Itoa(d.PeriodHours))
|
||||
}
|
||||
if util.Contain(d.Flags, DirectionFlagPRN) {
|
||||
builder.WriteString(" PRN")
|
||||
}
|
||||
if d.OptSchedule != "" && d.OptSchedule != OptScheduleDefault {
|
||||
if d.OptSchedule == OptScheduleWholeDose {
|
||||
builder.WriteString(" sched(whole)")
|
||||
}
|
||||
}
|
||||
|
||||
return d.Name, builder.String()
|
||||
}
|
||||
|
||||
type Dose struct {
|
||||
Time time.Time
|
||||
Dose int
|
||||
}
|
75
internal/health/parser_test.go
Normal file
75
internal/health/parser_test.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShortHandParser(t *testing.T) {
|
||||
|
||||
cases := [][]interface{}{
|
||||
{"Atorvastatin 10mg QD", &Direction{
|
||||
Name: "Atorvastatin",
|
||||
PeriodHours: 24,
|
||||
Dosage: 10,
|
||||
DosageUnit: "mg",
|
||||
Flags: make([]DirectionFlag, 0),
|
||||
OptSchedule: OptScheduleDefault,
|
||||
Disclaimer: DirectionDisclaimer,
|
||||
}},
|
||||
{"Atorvastatin 10mg TAB 10mg PO bid", &Direction{
|
||||
Name: "Atorvastatin 10mg TAB",
|
||||
PeriodHours: 12,
|
||||
Dosage: 10,
|
||||
DosageUnit: "mg",
|
||||
DosageRoute: "PO",
|
||||
Flags: make([]DirectionFlag, 0),
|
||||
OptSchedule: OptScheduleDefault,
|
||||
Disclaimer: DirectionDisclaimer,
|
||||
}},
|
||||
{"metformin 500mg qHS", &Direction{
|
||||
Name: "metformin",
|
||||
PeriodHours: 24,
|
||||
Dosage: 500,
|
||||
DosageUnit: "mg",
|
||||
Flags: []DirectionFlag{DirectionFlagHS},
|
||||
OptSchedule: OptScheduleDefault,
|
||||
Disclaimer: DirectionDisclaimer,
|
||||
}},
|
||||
{"Amphetamine 10mg qam", &Direction{
|
||||
Name: "Amphetamine",
|
||||
PeriodHours: 24,
|
||||
Dosage: 10,
|
||||
DosageUnit: "mg",
|
||||
Flags: []DirectionFlag{DirectionFlagAM},
|
||||
OptSchedule: OptScheduleDefault,
|
||||
Disclaimer: DirectionDisclaimer,
|
||||
}},
|
||||
{"Hydroxyzine 50mg qid prn sched(whole)", &Direction{
|
||||
Name: "Hydroxyzine",
|
||||
PeriodHours: 6,
|
||||
Dosage: 50,
|
||||
DosageUnit: "mg",
|
||||
Flags: []DirectionFlag{DirectionFlagPRN},
|
||||
OptSchedule: OptScheduleWholeDose,
|
||||
Disclaimer: DirectionDisclaimer,
|
||||
}}}
|
||||
|
||||
for _, c := range cases {
|
||||
input, expected := c[0].(string), c[1].(*Direction)
|
||||
actual, err := ParseShorthand(input)
|
||||
if expected == nil {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
expected.DirectionShorthand = actual.DirectionShorthand
|
||||
assert.Equal(t, expected, actual)
|
||||
name, encoded := actual.ShortHand()
|
||||
assert.Equal(t, expected.Name, name)
|
||||
encodedDecoded, err := ParseShorthand(name + " " + encoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, encodedDecoded)
|
||||
}
|
||||
}
|
||||
}
|
25
internal/util/daterange.go
Normal file
25
internal/util/daterange.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package util
|
||||
|
||||
import "time"
|
||||
|
||||
type DateRange struct {
|
||||
From time.Time
|
||||
To time.Time
|
||||
}
|
||||
|
||||
func NewDateRange(from time.Time, to time.Time) DateRange {
|
||||
from, _ = time.Parse("2006-01-02", from.Format("2006-01-02"))
|
||||
to, _ = time.Parse("2006-01-02", to.Format("2006-01-02"))
|
||||
if from.After(to) {
|
||||
from, to = to, from
|
||||
}
|
||||
return DateRange{from, to}
|
||||
}
|
||||
|
||||
func (d DateRange) Days() int {
|
||||
return int(d.To.Sub(d.From).Hours() / 24)
|
||||
}
|
||||
|
||||
func DateRangeAround(date time.Time, months int) DateRange {
|
||||
return NewDateRange(date.AddDate(0, -months, 0), date.AddDate(0, months, 0))
|
||||
}
|
|
@ -31,3 +31,11 @@ func AntiJoin[T comparable](a []T, b []T) []T {
|
|||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func Reverse[T any](a []T) []T {
|
||||
var result []T
|
||||
for i := len(a) - 1; i >= 0; i-- {
|
||||
result = append(result, a[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||
"github.com/eternal-flame-AD/yoake/internal/entertainment"
|
||||
"github.com/eternal-flame-AD/yoake/internal/health"
|
||||
"github.com/eternal-flame-AD/yoake/internal/servetpl"
|
||||
"github.com/eternal-flame-AD/yoake/internal/session"
|
||||
"github.com/eternal-flame-AD/yoake/internal/twilio"
|
||||
|
@ -70,6 +71,7 @@ func Init(hostname string, comm *comm.Communicator, database db.DB) {
|
|||
comm.RegisterAPIRoute(api.Group("/comm", logMiddleware("api_comm", nil)))
|
||||
auth.Register(api.Group("/auth", logMiddleware("api_auth", nil)))
|
||||
entertainment.Register(api.Group("/entertainment", logMiddleware("api_entertainment", nil)), database)
|
||||
health.Register(api.Group("/health", logMiddleware("api_health", nil)), database, comm)
|
||||
}
|
||||
|
||||
e.Use(
|
||||
|
|
472
webroot/includes/page-health-meds.tpl.html
Normal file
472
webroot/includes/page-health-meds.tpl.html
Normal file
|
@ -0,0 +1,472 @@
|
|||
<h1 class="page-header">Med Compliance</h1>
|
||||
|
||||
<div class="contianer">
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="card border">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Directions</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<style>
|
||||
.med-timeline {
|
||||
height: 2.5rem;
|
||||
flex-shrink: 3;
|
||||
}
|
||||
|
||||
.med-title {
|
||||
flex-grow: 1;
|
||||
min-width: 40%;
|
||||
}
|
||||
</style>
|
||||
<div class="accordion" id="med-directions-accordion">
|
||||
<div class="accordion-item d-none" id="med-direction-accordion-tpl" data-weight="0">
|
||||
<h2 class="accordion-header trima-procedure-hidden">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
style="flex-wrap: wrap">
|
||||
<span class="accordion-icon">
|
||||
{{template "/partials/sidebar-trima-procedure-logos.tpl.html" "1.25rem" }}
|
||||
|
||||
</span>
|
||||
<span class="med-title accordion-title">
|
||||
<div class="fw-bold">$name</div>
|
||||
<div>900mg PO qHS</div>
|
||||
</span>
|
||||
<svg id="timeline-tpl" class="med-timeline d-none" viewbox="0 0 300 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon class="arrow-flipped d-none" points="3,0 10,7 17,0" fill="" />
|
||||
<rect class="bar" width="280" x="10" y="7" height="6" fill="#8A6BBE" />
|
||||
<line class="line d-none" x1="10" y1="0" x2="10" y2="20" stroke="#373C38"
|
||||
stroke-width="1" />
|
||||
<polygon class="arrow d-none" points="3,20 10,14 17,20" fill="#58B2DC" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</h2>
|
||||
<div class="accordion-collapse collapse show">
|
||||
<div class="px-2 py-2 px-lg-5">
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<form class="med-take-form px-3">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-auto">
|
||||
<label for="dosage" class="form-label">Dosage Taken: </label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="number" class="form-control" id="dosage">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="submit" class="btn btn-primary mt-2" value="Submit">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
|
||||
<h5>History</h5>
|
||||
<div class="p-2 table-responsive" style="height:20em;overflow-y:scroll;">
|
||||
<table class="table table-striped compliance-log">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Dose</th>
|
||||
<th scope="col">Offset</th>
|
||||
<th scope="col">Offset (7 day)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="placeholder">
|
||||
<th class="placeholder">Loading...</th>
|
||||
<td class="placeholder"></td>
|
||||
<td class="placeholder"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="card border">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Manage</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="addMed" autocomplete="off">
|
||||
<div class="mb-3">
|
||||
<label id="med-shorthand-input" for="shorthand" class="form-label">Shorthand</label>
|
||||
<input type="text" class="form-control" id="shorthand"
|
||||
placeholder="Atorvastatin 10mg TAB 20mg PO qAM">
|
||||
|
||||
<label for="name" class="form-label">Name: </label>
|
||||
<input type="text" class="form-control" id="name" placeholder="Atorvastatin 10mg TAB">
|
||||
<label for="dosage" class="form-label">Dosage: </label>
|
||||
<input type="number" class="form-control" id="dosage" placeholder="20">
|
||||
<label for="dosage_unit" class="form-label">Dosage Unit: </label>
|
||||
<input type="text" class="form-control" id="dosage_unit" placeholder="mg">
|
||||
<label for="dosage_route" class="form-label">Dosage Route: </label>
|
||||
<input type="text" class="form-control" id="dosage_route" placeholder="PO">
|
||||
<label for="period_hours" class="form-label">Period (Hours): </label>
|
||||
<input type="number" class="form-control" id="period_hours" placeholder="24">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="flags" class="form-label">Flags:</label>
|
||||
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="flags-qam" name="qAM" value="qam">
|
||||
<label for="flags-qam" class="form-check-label">qAM</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="flags-qhs" name="qHS" value="qhs">
|
||||
<label for="flags-qhs" class="form-check-label">qHS</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="flags-prn" name="PRN" value="prn">
|
||||
<label for="flags-prn" class="form-check-label">PRN</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="flags" class="form-label">Schedule:</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" class="form-check-input" id="schedule-default" name="schedule"
|
||||
value="default">
|
||||
<label for="schedule-default" class="form-check-label">Default</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" class="form-check-input" id="schedule-whole" name="schedule"
|
||||
value="whole">
|
||||
<label for="schedule-whole" class="form-check-label">Whole Dose</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
|
||||
</form>
|
||||
<script>
|
||||
$("#addMed #shorthand").on('change', function () {
|
||||
let shorthand = $(this).val();
|
||||
$.ajax({
|
||||
url: "/api/health/meds/shorthand/parse?shorthand=" + encodeURIComponent(shorthand),
|
||||
success: function (data) {
|
||||
$("#addMed #name").val(data.name);
|
||||
$("#addMed #dosage").val(data.dosage);
|
||||
$("#addMed #dosage_unit").val(data.dosage_unit);
|
||||
$("#addMed #dosage_route").val(data.dosage_route);
|
||||
$("#addMed #period_hours").val(data.period_hours);
|
||||
$("#addMed #flags-qam").prop("checked", data.flags.includes("qam"));
|
||||
$("#addMed #flags-qhs").prop("checked", data.flags.includes("qhs"));
|
||||
$("#addMed #flags-prn").prop("checked", data.flags.includes("prn"));
|
||||
$("#addMed #schedule-default").prop("checked", data.schedule == "default");
|
||||
$("#addMed #schedule-whole").prop("checked", data.schedule == "whole");
|
||||
}
|
||||
|
||||
})
|
||||
});
|
||||
$("#addMed").on("submit", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
let name = $("#addMed #name").val();
|
||||
let dosage = $("#addMed #dosage").val();
|
||||
let dosage_unit = $("#addMed #dosage_unit").val();
|
||||
let dosage_route = $("#addMed #dosage_route").val();
|
||||
let period_hours = $("#addMed #period_hours").val();
|
||||
let flags = [];
|
||||
if ($("#addMed #flags-qam").prop("checked")) {
|
||||
flags.push("qam");
|
||||
}
|
||||
if ($("#addMed #flags-qhs").prop("checked")) {
|
||||
flags.push("qhs");
|
||||
}
|
||||
if ($("#addMed #flags-prn").prop("checked")) {
|
||||
flags.push("prn");
|
||||
}
|
||||
let schedule = $("#addMed input[name=schedule]:checked").val();
|
||||
$.ajax({
|
||||
url: "/api/health/meds/directions",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({
|
||||
name: name,
|
||||
dosage: parseInt(dosage),
|
||||
dosage_unit: dosage_unit,
|
||||
dosage_route: dosage_route,
|
||||
period_hours: parseInt(period_hours),
|
||||
flags: flags,
|
||||
schedule: schedule
|
||||
}),
|
||||
success: function (data) {
|
||||
window.location.reload();
|
||||
},
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
"use strict";
|
||||
const medKeyName = name =>
|
||||
name.split(" ")[0].toLowerCase();
|
||||
|
||||
const dirAccEl = document.getElementById("med-directions-accordion")
|
||||
const dirAccTpl = document.getElementById("med-direction-accordion-tpl")
|
||||
|
||||
let accByMeds = {};
|
||||
|
||||
const writeAccordion = async (medList, initial) => {
|
||||
await Promise.all(
|
||||
medList.map(med => new Promise((resolve, reject) => {
|
||||
{
|
||||
const prn = med.flags.includes("prn");
|
||||
let id = "med-direction-accordion-" + med.name
|
||||
let accEl = document.getElementById(id)
|
||||
if (!accEl) {
|
||||
accEl = dirAccTpl.cloneNode(true)
|
||||
accEl.id = id
|
||||
}
|
||||
const medTakeForm = accEl.querySelector(".med-take-form")
|
||||
medTakeForm.querySelector("input#dosage").value = med.dosage
|
||||
const medTimeline = (() => {
|
||||
let timeline = accEl.querySelector("#timeline")
|
||||
const medTimelineTpl = accEl.querySelector("#timeline-tpl")
|
||||
if (timeline) {
|
||||
timeline.parentElement.removeChild(timeline)
|
||||
}
|
||||
timeline = medTimelineTpl.cloneNode(true)
|
||||
medTimelineTpl.parentElement.append(timeline)
|
||||
timeline.id = "timeline"
|
||||
timeline.classList.remove("d-none")
|
||||
return timeline
|
||||
})()
|
||||
medTakeForm.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
let dosage = medTakeForm.querySelector("input#dosage").value
|
||||
if (confirm("Really Submit?"))
|
||||
$.ajax({
|
||||
url: "/api/health/meds/compliance/log",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({
|
||||
med_keyname: medKeyName(med.name),
|
||||
actual: {
|
||||
time: dayjs().format(),
|
||||
dose: parseInt(dosage)
|
||||
},
|
||||
}),
|
||||
success: function (data) {
|
||||
window.location.reload();
|
||||
},
|
||||
})
|
||||
}
|
||||
accByMeds[medKeyName(med.name)] = accEl
|
||||
accEl.id = "med-direction-accordion-" + med.name
|
||||
let title = accEl.querySelector(".accordion-title")
|
||||
title.querySelector("div:first-child").innerText = med.name
|
||||
title.querySelector("div:last-child").innerText = med.shorthand
|
||||
const bodyId = accEl.querySelector(".accordion-collapse").id = "med-direction-body-" + medKeyName(med.name)
|
||||
accEl.querySelector(".accordion-button").setAttribute("data-bs-target", "#" + bodyId)
|
||||
accEl.querySelector(".accordion-button").setAttribute("aria-controls", bodyId)
|
||||
|
||||
$.ajax({
|
||||
url: "/api/health/meds/compliance/med/" + medKeyName(med.name) + "/project",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
error: function (xhr, status, error) {
|
||||
reject(error)
|
||||
},
|
||||
success: function (data) {
|
||||
let icon = accEl.querySelector(".accordion-icon")
|
||||
let important = false
|
||||
let available = false
|
||||
icon.setAttribute("class", "accordion-icon")
|
||||
if (data.dose_offset < -0.2 || dayjs().isAfter(dayjs(data.expected.time).add(1, "day"))) {
|
||||
accEl.setAttribute("data-weight", prn ? 5 : 15)
|
||||
icon.classList.add("trima-procedure-ineligible")
|
||||
} else if (data.dose_offset >= 0 && !prn) {
|
||||
available = true
|
||||
accEl.setAttribute("data-weight", 50)
|
||||
important = true
|
||||
icon.classList.add("trima-procedure-optimal")
|
||||
} else {
|
||||
available = true
|
||||
accEl.setAttribute("data-weight", prn ? 10 : 20)
|
||||
icon.classList.add("trima-procedure-valid")
|
||||
}
|
||||
if (initial) {
|
||||
accEl.querySelector(".accordion-collapse").classList[important ? "add" : "remove"]("show")
|
||||
accEl.querySelector(".accordion-button").classList[important ? "remove" : "add"]("collapsed")
|
||||
}
|
||||
|
||||
medTakeForm.querySelector("input#dosage").value = data.expected.dose
|
||||
|
||||
$.ajax({
|
||||
url: "/api/health/meds/compliance/med/" + medKeyName(med.name) + "/log",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
error: function (xhr, status, error) {
|
||||
reject(error)
|
||||
},
|
||||
success: function (logs) {
|
||||
const tbody = accEl.querySelector(".compliance-log tbody");
|
||||
tbody.innerHTML = "";
|
||||
let projectedTr = document.createElement("tr");
|
||||
projectedTr.classList.add("table-primary");
|
||||
projectedTr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td>`;
|
||||
labelTimeElement(projectedTr.children[0], data.expected.time);
|
||||
projectedTr.children[1].innerText = `${data.expected.dose} ${med.dosage_unit} (${prn ? "available" : "scheduled"})`;
|
||||
tbody.appendChild(projectedTr);
|
||||
|
||||
logs.forEach(log => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td>`;
|
||||
labelTimeElement(tr.children[0], log.actual.time)
|
||||
tr.children[1].innerText = `${log.actual.dose}/${log.expected.dose} ${med.dosage_unit}`;
|
||||
if (log.actual.dose !== log.expected.dose) {
|
||||
tr.children[1].classList.add("table-warning");
|
||||
} else {
|
||||
tr.children[1].classList.add("table-success");
|
||||
}
|
||||
tr.children[2].innerText = log.dose_offset.toFixed(2);
|
||||
if (Math.abs(log.dose_offset) > 1) {
|
||||
tr.children[2].classList.add("table-danger");
|
||||
} else if (Math.abs(log.dose_offset) > 0.5) {
|
||||
tr.children[2].classList.add("table-warning");
|
||||
} else {
|
||||
tr.children[2].classList.add("table-success");
|
||||
}
|
||||
|
||||
// compute 7 day offset
|
||||
const weekFrom = dayjs(log.actual.time).subtract(7, 'day')
|
||||
let offset = 0
|
||||
logs.forEach(log => {
|
||||
if (dayjs(log.actual.time).isAfter(weekFrom)) {
|
||||
offset += log.dose_offset
|
||||
}
|
||||
})
|
||||
|
||||
tr.children[3].innerText = offset.toFixed(2);
|
||||
if (Math.abs(offset) > 1) {
|
||||
tr.children[3].classList.add("table-danger");
|
||||
} else if (Math.abs(offset) > 0.5) {
|
||||
tr.children[3].classList.add("table-warning");
|
||||
} else {
|
||||
tr.children[3].classList.add("table-success");
|
||||
}
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// compute timeline
|
||||
const timelineStart = dayjs().subtract(med.period_hours * 3, 'hour')
|
||||
const timelineEnd = dayjs().add(med.period_hours, 'hour')
|
||||
let timelineDoses = [
|
||||
{ "type": "now", "time": dayjs(), "dose": 0 },
|
||||
]
|
||||
if (!(prn && available)) {
|
||||
timelineDoses.push({ "type": "projected", "time": data.expected.time, "dose": data.expected.dose })
|
||||
}
|
||||
|
||||
logs.forEach(log => {
|
||||
timelineDoses.push({ "type": "actual", "time": log.actual.time, "dose": log.actual.dose })
|
||||
timelineDoses.push({ "type": "expected", "time": log.expected.time, "dose": log.expected.dose })
|
||||
})
|
||||
|
||||
if (prn)
|
||||
$(medTimeline).find(".bar").attr("fill", "#B19693");
|
||||
|
||||
timelineDoses = timelineDoses.map(dose => {
|
||||
dose.time = dayjs(dose.time)
|
||||
dose.timerel = dose.time.diff(timelineStart) / timelineEnd.diff(timelineStart)
|
||||
return dose
|
||||
}).filter(dose => {
|
||||
return dose.time.isAfter(timelineStart) && dose.time.isBefore(timelineEnd)
|
||||
}).forEach(dose => {
|
||||
console.log(dose)
|
||||
let baseClass = ""
|
||||
let fill = ""
|
||||
|
||||
switch (dose.type) {
|
||||
case "projected":
|
||||
baseClass = "arrow-flipped"
|
||||
fill = "#ECB88A"
|
||||
break
|
||||
case "actual":
|
||||
baseClass = "arrow"
|
||||
fill = "#7BA23F"
|
||||
break
|
||||
case "expected":
|
||||
baseClass = "arrow-flipped"
|
||||
fill = "#58B2DC"
|
||||
break
|
||||
case "now":
|
||||
baseClass = "line"
|
||||
fill = ""
|
||||
break
|
||||
}
|
||||
let arrow = $("." + baseClass).clone()
|
||||
if (fill)
|
||||
arrow.attr("fill", fill)
|
||||
arrow.removeClass("d-none")
|
||||
const fullrange = 280
|
||||
arrow.attr("transform", "translate(" + (dose.timerel * fullrange) + ", 0)")
|
||||
$(medTimeline).append(arrow)
|
||||
})
|
||||
|
||||
accEl.classList.remove("d-none")
|
||||
let inserted = false
|
||||
for (const node of dirAccEl.children) {
|
||||
if (node.classList.contains("accordion-item"))
|
||||
if (parseInt(node.getAttribute("data-weight")) <
|
||||
parseInt(accEl.getAttribute("data-weight"))) {
|
||||
dirAccEl.insertBefore(accEl, node)
|
||||
inserted = true
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
dirAccEl.appendChild(accEl)
|
||||
}
|
||||
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
let updateTimer;
|
||||
document.addEventListener("sidebar-activate", e => {
|
||||
if (e.detail.page == "health-meds") {
|
||||
$.ajax({
|
||||
url: "/api/health/meds/directions",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
writeAccordion(data, true).then(() => {
|
||||
updateTimer = setInterval(() => writeAccordion(data).catch(err => {
|
||||
throw err
|
||||
}), 300 * 1000)
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
clearInterval(updateTimer);
|
||||
}
|
||||
})
|
||||
})()
|
||||
</script>
|
|
@ -63,7 +63,7 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link" href="#entertainment-youtube" data-state-bind="entertainment-youtube">
|
||||
<span class="sidebar-trima-procedure">
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" }}
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" "2.5rem" }}
|
||||
</span>Youtube Playlist
|
||||
</a>
|
||||
</li>
|
||||
|
@ -81,12 +81,29 @@
|
|||
<a class="nav-link" href="#canvas-grades" data-state-bind="canvas-grades"
|
||||
data-auth-bind="admin">
|
||||
<span class="sidebar-trima-procedure">
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" }}
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" "2.5rem" }}
|
||||
</span>Grades
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6
|
||||
class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
|
||||
<span>Health</span>
|
||||
<a class="link-secondary" href="#" aria-label="Health">
|
||||
<span data-feather="plus-circle" class="align-text-bottom"></span>
|
||||
</a>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#health-meds" data-state-bind="health-meds"
|
||||
data-auth-bind="admin">
|
||||
<span class="sidebar-trima-procedure">
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" "2.5rem" }}
|
||||
</span>Med Compliance
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6
|
||||
class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
|
||||
|
@ -99,7 +116,7 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link" href="#gotify-ui" data-state-bind="gotify-ui">
|
||||
<span class="sidebar-trima-procedure">
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" }}
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" "2.5rem" }}
|
||||
</span>Gotify UI
|
||||
</a>
|
||||
</li>
|
||||
|
@ -116,7 +133,7 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link" href="#vault-ui" data-state-bind="vault-ui">
|
||||
<span class="sidebar-trima-procedure">
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" }}
|
||||
{{ template "/partials/sidebar-trima-procedure-logos.tpl.html" "2.5rem" }}
|
||||
</span>Vault UI
|
||||
</a>
|
||||
</li>
|
||||
|
@ -157,6 +174,11 @@
|
|||
{{ template "/includes/page-vault-ui.tpl.html" . }}
|
||||
</main>
|
||||
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 router-page" id="page-health-meds"
|
||||
data-title="Med Compliance">
|
||||
{{ template "/includes/page-health-meds.tpl.html" . }}
|
||||
</main>
|
||||
|
||||
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 router-page" id="page-not-found" data-title="Not Found">
|
||||
Not Found
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
function doNow(fn) {
|
||||
fn();
|
||||
return fn;
|
||||
}
|
||||
|
||||
function labelTimeElement(tag, time) {
|
||||
time = dayjs(time);
|
||||
|
||||
if (tag.innerText == "")
|
||||
tag.innerText = time.fromNow();
|
||||
tag.setAttribute("data-bs-toggle", "tooltip");
|
||||
tag.setAttribute("data-bs-title", time.format("L LT"));
|
||||
new bootstrap.Tooltip(tag);
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
{{ $size := 50 }}
|
||||
{{ if . }}
|
||||
{{ $size = .}}
|
||||
{{ end }}
|
||||
{{range $key, $name:= (parse_json `
|
||||
{
|
||||
"ineligible": "icon_t_vista_procedure_ineligible.gif",
|
||||
|
@ -11,7 +15,8 @@
|
|||
"icon_t_vista_procedure_valid.gif"
|
||||
}
|
||||
`)}}
|
||||
<img aria-hidden="true" class="trima-procedure trima-procedure-{{$key}}" src={{ trima_img $name "url" }}>
|
||||
<img aria-hidden="true" style="height:{{$size}};width:{{$size}};" class="trima-procedure trima-procedure-{{$key}}"
|
||||
src={{ trima_img $name "url" }}>
|
||||
|
||||
|
||||
{{ end }}
|
Loading…
Reference in a new issue