Compare commits

..

1453 commits
0.0.11 ... main

Author SHA1 Message Date
970cd568dc
Add generated jet tables and models
These will change with schema changes
2026-05-09 01:54:12 +00:00
935800e334
Switch to using the Gleipnir fork of jet
Because we need those sweet, sweet geometry columns
2026-05-09 01:48:56 +00:00
b5bc54b7f4
Start adding context resources to communications
These will contain URIs for anything related to the communication
2026-05-09 01:39:08 +00:00
0301545df9
Go back to replacing jet for now
I need it for my geometry column types
2026-05-09 01:38:36 +00:00
e36b512908
Stop ignoring any paths with 'nidus-sync' in them
Dumb oversight on my part
2026-05-09 01:38:07 +00:00
aa3c6e6209
Don't use custom jet, tidy module 2026-05-09 01:22:30 +00:00
93b69c4cbb
Remove dead query.go at project root
The QueryWriter interface and queryToString function had zero callers.
The commented-out insertQueryToString was a Bob remnant. The io import
was only used in this file.
2026-05-09 01:05:29 +00:00
7ffa2e891b
Remove dead esbuild build.js and flake.nix dependency
build.js was an esbuild-based build script from the pre-Vite era (March 2026).
It is not referenced by package.json, CI, or any config. Vite is used for
both sync and rmo builds. Also dropped pkgs.esbuild from flake.nix devShell.
2026-05-09 01:03:51 +00:00
be99baf64c
Remove unused tomtom/ integration
TomTom was added Feb 2026 for routing but was never imported outside
its own directory. Stadia Maps is now the geocoding and tile provider.
No references in go.mod, go.sum, or any Go file.
2026-05-09 01:01:49 +00:00
8592659432
Test of agent capabilities:
"I'd like you to take a look at the project nidus-sync. It has been developed over the past 6 months and had several different architectural approaches that have evolved over time. The git history may be especially useful to see the
evolution. Write up a report of what you understand has happend, with approximate timelines. This should go in HISTORY.md. Determine which changes are incomplete - libraries or approaches that are no longer preferred, but not fully
removed yet. Create another file, CLEANUP.md, which lists out cleanup
efforts that should be done to completely remove these older,
less-preferred parts of the code."
2026-05-09 00:54:39 +00:00
1b6fac3313
Distinguish between communication stubs and full resources
This is useful so I don't have to pull together the entirety of the log
for a communication list, which would be much more expensive.
2026-05-09 00:03:11 +00:00
01f35b603e
Add centralized error handler for sync Vue app 2026-05-08 23:33:49 +00:00
da90401b2d
Push location config to client
We'll let the default stay the default.
2026-05-08 22:48:51 +00:00
28cf7683a7
Pass-through an address shim with whatever data we have 2026-05-08 22:45:26 +00:00
d1ba2f53fa
Fix setting address on compliance reports
This error was subtle. First, we want to set the GID and raw content
directly using the updater instead of doing two round trips because we
can. Second, we want to do some geocoding if the address isn't already
in the system. Likely it is, because the frontend would have requested a
geocode, but it's possible that it isn't.
2026-05-08 22:43:57 +00:00
24a3610c4c
Correctly build updaters with New
Otherwise we have nil columns
2026-05-08 22:22:52 +00:00
7da653efc6
Avoid a DB query if there are no address IDs 2026-05-08 22:21:56 +00:00
735a9dc1d2
Properly close rows on empty results
I we don't do this we get "conn busy" errors.
2026-05-08 22:21:27 +00:00
f2585c569c
Woops, actually set all columns on compliance because it doesn't have a serial key 2026-05-08 01:08:06 +00:00
0fc46d5916
Only set mutable columns on insert
Because we don't want to set ID and other primary keys
2026-05-08 00:56:55 +00:00
61ad3fbe45
Remove string-based queries for public report data
Use the new jet hotness
2026-05-07 23:22:50 +00:00
12213fb31b
Remove string-only references to location_* generated columns 2026-05-07 17:01:54 +00:00
7a361a330d
Remove now-extraneous latitude/longitude generated columns
Now that we can pull out the geometry directly into a go object we don't
need these and they complicate our insertions
2026-05-07 16:38:42 +00:00
34a136eba5
Move user to compliance complete page for submitted reports 2026-05-07 16:17:00 +00:00
fcd95f1a25
Get back to compiling, but using new jet for publicreport
This was an epically long change, and a terrible idea, but it compiles.
This was essentially a cascade that came about because I can't blend jet
and bob in the same transaction. In for a penny, I guess...
2026-05-07 10:39:17 +00:00
a95e44cf42
Use transactions to set the communication status changes
Not doing it yet, but soon we'll do log entries for them.
2026-05-04 20:57:50 +00:00
040ab106b4
Fix failing to set timestamp in mark query
I accidentally didn't understand how this API works.
2026-05-04 20:30:36 +00:00
5f3fcc2b3e
Fix a bunch of not-checking-error lints 2026-05-04 20:29:02 +00:00
114aec73ed
Fix setting timestamp for when action is taken 2026-05-04 20:09:56 +00:00
60bf09e813
Avoid emitting error on transaction rollback that's complete
It's on purpose
2026-05-04 19:53:36 +00:00
347f62bd6d
Fix method on marking communications 2026-05-04 19:43:37 +00:00
878f0e9bcf
Try a different way to limit linting 2026-05-04 19:43:20 +00:00
18db17fe0b
Don't lint every go file on every commit.
Faster commits, less redundancy
2026-05-04 19:40:12 +00:00
b53c908b55
Fix warning from sentry setup 2026-05-04 19:39:17 +00:00
dc2fee3a9d
Fix selecting items in the communication list 2026-05-04 19:39:03 +00:00
387be40076
Return ID as a string from API
Because they are opaque, not something to math
2026-05-04 19:37:05 +00:00
3153e8bf13
Initial work marking communications
And a bunch of lint fixes
2026-05-04 19:07:29 +00:00
67c99436d1
Properly set submitted on PUT, return new status properties on comms 2026-05-02 00:41:31 +00:00
431435f8bd
Set the organization on inserted communications 2026-05-02 00:38:38 +00:00
57dc2023cd
Remove unused submit function 2026-05-02 00:38:12 +00:00
d6b664d84a
Return the communication we create
...or else it'll be empty.
2026-05-02 00:37:51 +00:00
52d4c47e43
Fix lint errors related to not checking errors 2026-05-02 00:37:28 +00:00
7f71ff9a2e
Send submit PUT on compliance report flow, create communication then
This makes it so that people don't see compliance reports as they're
being formulated in the communication workbench
2026-05-01 21:27:17 +00:00
a82732a49c
Return communication database rows from communication API
This is a pretty big refactor of how communication works to start moving
us in the direction we want to go long-term. This adds the new
communication row and migrates existing reports to add rows for
communication.

There's also a bunch of automatic fixes from the new linter. I should
have added them separately, but whatever.
2026-05-01 21:00:23 +00:00
a6ce0b7e67
Add golangci-lint to lefthook workflow 2026-05-01 17:36:32 +00:00
bab3200b6c
Port all of the arcgis schema to using jet
Have not tested anything at this point, it just compiles.
2026-05-01 17:28:33 +00:00
89ed2003fa
Ignore custom jet schema binary 2026-05-01 15:13:50 +00:00
a82b2b8cb8
Add new communication table
It allows us to track when communication tasks are complete, and
information about how they were completed, separate from the entries
that created the tasks in the first place (reports, emails, texts)
2026-05-01 15:13:05 +00:00
6a47302192
Create custom jet template for both stadia and arcgis 2026-05-01 15:11:20 +00:00
0e0b2489e6
Get beginnings of custom column type working 2026-05-01 06:27:26 +00:00
9ef4dad27c
Initial custom jet generator
I'll need it for Postgis data types
2026-05-01 05:45:45 +00:00
e5a84e09a8
Initial working version of using jet for SQL building 2026-05-01 05:11:28 +00:00
4bd62b3567
Fix publicreport store name pollution
This was causing a request to be made to the wrong API endpoint by going
to /api/publicreport instead of /api/rm/publicreport which doesn't work
on RMO's hostname.
2026-05-01 02:37:54 +00:00
503cde6063
Init sentry first, then mount the app
Gets rid of a warning from the Sentry SDK
2026-05-01 01:52:44 +00:00
8757f1cda3
Fix loading on status page
It was infinite looping in the computed value for report
2026-05-01 01:52:11 +00:00
ace2557a60
Fix navigation from RMO status page on report table 2026-05-01 01:43:57 +00:00
537d5c9133
Save address input if the user clicks the "back" button. 2026-04-30 16:46:28 +00:00
00d26a684a
Handle EXIF location data set to "NaN"
Probably Android's new privacy thing. Jerks.
2026-04-30 15:34:08 +00:00
1cfe51f894
Actually check the error state for saving an image 2026-04-30 14:27:45 +00:00
43edca7093
Add new response template 2026-04-30 13:58:31 +00:00
839ed138ca
Ignore vite deps 2026-04-30 03:48:53 +00:00
797067ee38
Refuse to send compliance letters to addresses without a postal code 2026-04-30 03:09:42 +00:00
e45e05f337
Copy vite build output to frontend in nix package
Then we can upload the symbols when we run
2026-04-30 03:09:06 +00:00
b8a9ecb253
Fix warning from vite about concern having multiple root entities 2026-04-30 00:07:37 +00:00
0c464a9963
Get working sentry for the UI
Previously it almost, but didn't quite work. Now it actually works, but
the stack traces are minified.
2026-04-29 23:59:34 +00:00
2f6cbe59eb
Add root API to RMO api
For getting sentry integration information
2026-04-29 23:58:49 +00:00
33ecfce313
Use RMO publicreport store in compliance and district report creation 2026-04-29 22:36:33 +00:00
9229725300
Consistently show loading state on compliance flow button 2026-04-29 22:27:08 +00:00
89eca2ddf9
One more attempt to handle report null-ness 2026-04-29 20:58:47 +00:00
a1b2d580a8
Return nil through on by id compliance 2026-04-29 20:37:36 +00:00
c6cb645453
Don't error out on missing report 2026-04-29 20:24:41 +00:00
7e79308868
Don't return an error when report doesn't exist 2026-04-29 20:18:36 +00:00
da75aeecf2
Allow multiple posts of address report
Since this is the first step of scanning the mailer.
2026-04-29 19:30:43 +00:00
af39a73e8f
Add address raw content to report
This populates the address in the compliance flow UI
2026-04-29 19:30:38 +00:00
53ce100859
Fix copy-paste error on mailer store 2026-04-29 19:17:20 +00:00
364b4ddc32
Remove click timeout on MapLocator
This was added to try to fix scrolling passed the map on phones.
Instead, it just confuses click-and-drag. Instead we rely on the
lock/unlock overlay for the map to make scrolling passed work.
2026-04-29 15:58:57 +00:00
06fda0554c
Fix link to report status 2026-04-29 15:55:27 +00:00
0375f666d6
Remove detailed identification guide link
It didn't go anywhere.
2026-04-29 15:54:31 +00:00
aefb5ec6bd
Fix tooltips on water report page 2026-04-29 15:37:38 +00:00
0a0e6f6301
Prevent nuisance and water form submission on enter 2026-04-29 15:31:04 +00:00
ab5dd13fcb
Make "Submit Anothe Report" go to root so they can choose the type 2026-04-29 15:21:54 +00:00
d67c54c6e7
Set authenticated after sigin completes without error 2026-04-29 15:04:44 +00:00
4bb37c5ab3
Reconnect SSE event stream after shutdown
Otherwise we'll never know we have updates
2026-04-29 15:02:18 +00:00
2fbceb11e3
Don't bail on district match early, check address
This is the other half of doing proper district match via raw address -
we have to use the address if available for looking up a district.
2026-04-29 15:01:35 +00:00
524353bfa1
Geocode address if we only have a raw value
This will help with matching when the user does not select a suggested
address.
2026-04-29 13:55:10 +00:00
822dad5352
Don't require systemd sockets in dev mode
Because otherwise I can't run the program on my dev server
2026-04-29 13:54:48 +00:00
5ac778ea53
Make the shutdown event a status message
That way our status handlers on the frontend will know what additional
data is available
2026-04-29 13:54:17 +00:00
f3af19f03a
Add systemd activation sockets for downtime-free deploys 2026-04-28 23:24:19 +00:00
bd3d3881f5
Increase server shutdown timeout
We shouldn't see this unless we fail to get everything closed down.
2026-04-28 22:37:43 +00:00
a17544bb4b
Downgrade errors on server shutdown 2026-04-28 22:25:31 +00:00
a101ff3cc9
Suppress errors from canceled context on DB notification goroutine 2026-04-28 22:24:15 +00:00
a5b9ee0c6c
Actually check error code on DB query for audio post 2026-04-28 22:18:41 +00:00
4a90917645
Add more detail to address creation failure 2026-04-28 22:16:22 +00:00
d5d6201177
Improve error response from Lob integration 2026-04-28 22:14:05 +00:00
309f8fe2c5
Downgrade failure to get admin info to warning
To clear out Glitchtip a bit
2026-04-28 22:10:39 +00:00
bf6b5dcb17
Fix privacy policy render 2026-04-28 21:42:48 +00:00
4ed005fb37
Move map as bounds change on communication workspace 2026-04-28 20:37:38 +00:00
4fcb184286
Make communication map location reactive
Prevents stateful errors like failing to update bounds when the API
fetches a new value for a report
2026-04-28 20:31:40 +00:00
2911c7b215
Add a parameter to track which communication is selected 2026-04-28 20:09:13 +00:00
ff668c223b
Add update notification when version changes 2026-04-28 17:09:43 +00:00
52c41e29d8
Distinguish between status messages and resource messages in SSE 2026-04-28 17:06:21 +00:00
38359e20e9
Use auto build version info for embedding version information
This is better, integrates with git, gives us more detail, and I don't
have to explicitly pass it around everywhere.
2026-04-28 16:36:48 +00:00
20bf272746
Clean up communication list display 2026-04-28 15:30:15 +00:00
060d0dd95f
Add compliance card detail information display 2026-04-28 15:22:20 +00:00
72626e8dd0
Fix incorrectly passing public status to platform for nuisance 2026-04-28 14:56:06 +00:00
b68d93ec91
Load communication reports asynchronously
This solves some problems created by making the publicreport part of the
communication API consistent. There are a lot of optimizations still on
the table with this one, but for now I need to get this out.
2026-04-28 14:49:02 +00:00
6e3d079c46
Immediately mark session authenticated on successful signin 2026-04-28 14:37:33 +00:00
878b43c0a6
Fix text log logo for outgoing 2026-04-28 07:45:51 +00:00
175fd8d0fb
Fix water public ByIDGet uri generation 2026-04-28 07:34:54 +00:00
dba8b6c475
Consistently use the correct public URI for public reports 2026-04-28 07:12:12 +00:00
32a0d895c4
Fix missing RMO report store 2026-04-28 06:54:16 +00:00
4ae0410930
Fix various inter-linkings of public report paths 2026-04-28 06:53:58 +00:00
8bdd18649d
Separate out a public and non-public halves to publicreport APIs
This prevents us from leaking text messaging details on public
endpoints.
2026-04-28 06:36:55 +00:00
8fcd926d43
Format report IDs with hyphens 2026-04-28 05:33:53 +00:00
68adab88bc
Re-add notifications registered page 2026-04-28 05:30:08 +00:00
82b313f62f
Add text message log to report display 2026-04-28 05:15:01 +00:00
4ce91d77d4
Add text message history to acitivity log 2026-04-28 01:12:18 +00:00
6350aa00d5
Populate report URI and district on communication list 2026-04-27 23:15:33 +00:00
909665ab6c
Use new map system for communication pane, fix location markers 2026-04-27 20:53:46 +00:00
1dba58472b
Add support to communication list view for compliance entries 2026-04-27 20:06:44 +00:00
9c392e5791
Make public report card name consistent with other components 2026-04-27 19:53:22 +00:00
63ebe382b6
Properly convert communication API objects
Gets rid of a warning when showing relative time.
2026-04-27 19:50:47 +00:00
a2b8527d91
Track user location with map and address data
This is useful because everywhere that we use the AddressAndMapLocator
component we also want to use the user's location and we want to zoom
the map based on their location. Instead of tracking this externally in
3 places we just pull it into the component.
2026-04-27 19:44:25 +00:00
3867737fcc
Track camera changes during map load
This is necessary so that we can frame the map at any time in client
code, like with the user's location data, and still end up with the
correct location.
2026-04-27 19:44:25 +00:00
937953f2a2
Produce a raw address value from geocode requests
This makes it so that the frontend doesn't have to calculate what to
display
2026-04-27 19:44:25 +00:00
96498c01bf
Add API for getting just the closest reverse geocoded answer
Because we don't care about anything that is nearby when the user clicks
on the map, we just want the closest thing.
2026-04-27 19:44:25 +00:00
b92697b8c8
Remove erroneous "MapCell' component
Eliminates a warning in the build step
2026-04-27 16:23:46 +00:00
ffe427564b
Add email and phone display to communications workbench 2026-04-27 16:23:31 +00:00
be8d92d7ae
Add 'submitted' field to compliance reports 2026-04-27 16:23:16 +00:00
8a05ba2faf
Initial reimplementation in VueJS of address or report suggestion 2026-04-25 00:17:35 +00:00
c783ab7942
Add report rendering table to status page 2026-04-24 23:06:07 +00:00
5e638bdf1d
Remove hidden water inputs, add missing duration input 2026-04-24 22:23:52 +00:00
203d2014b0
Show map with nuisance and water on status page
Leverages the new declarative map logic. Still missing a bunch of
features
2026-04-24 22:20:01 +00:00
3bfcfff1eb
Navigate to cell detail page on cell click 2026-04-24 13:48:00 +00:00
e5080eaaf6
Add click event for cells on the dash map 2026-04-24 13:23:03 +00:00
a88aa4c8a0
Change cursor when the user hovers over a layer 2026-04-24 00:36:18 +00:00
6992031007
Add callback for when the mouse enters or leaves a layer 2026-04-24 00:31:03 +00:00
c1a8249dc0
Don't remove source and layer on unregister
I'm not convinced this is necessary since we are freeing the map itself
and its causing a crash on navigation away.
2026-04-23 23:47:16 +00:00
77283b3654
Add parcel data to overview heatmap 2026-04-23 23:46:31 +00:00
f1e4aca9b8
Set bounds to default if the district doesn't have a service area 2026-04-23 23:38:12 +00:00
963254973b
Move map-utils into map/
Namespacing matters
2026-04-23 23:31:50 +00:00
cad01e689e
Initial working genericized map implementation
This shows dynamically adding layers and sources and actually reads from
them!
2026-04-23 23:02:53 +00:00
c6282c9f5e
Default to setting AddressGid
So we don't run afoul of the nullable constraint
2026-04-23 22:41:22 +00:00
c8989237b0
Fix reference to address number. Again. 2026-04-23 21:39:13 +00:00
516dd6f429
Don't show "send compliance mailer" if org isn't configured for Lob 2026-04-23 15:50:58 +00:00
72a8ed5c16
Improve signin messaging 2026-04-23 15:24:06 +00:00
b4e6bac566
Fix reference to number_ column 2026-04-23 13:53:22 +00:00
cc59ccb9b5
Fix reference to address number column 2026-04-23 00:34:51 +00:00
1ddba5ebb1
Fix qr code generator functions 2026-04-23 00:30:42 +00:00
582aa952e4
Remove template test 2026-04-23 00:30:35 +00:00
10dc5c0bd7
Move qr-code generation to the API 2026-04-23 00:28:31 +00:00
7be8b428e4
Remove remaining sync mocks 2026-04-22 23:02:21 +00:00
1d266c88c1
Fix initial view of markers on map load
The issue here was that "fitBounds" doesn't work before the map is
loaded, we have to use the map constructor to set the location.
Therefore it makes no sense to even attempt these operations internally
before loading.
2026-04-22 22:43:16 +00:00
5caa9d8c7a
Populate address if we have enough data on compliance address form 2026-04-22 22:42:26 +00:00
b0170b20d5
Update fetching address number to match new types.Address pattern
This matches what we get by using the models column definition directly.
2026-04-22 22:20:42 +00:00
f24a583e2e
Add beginning of cell detail page 2026-04-22 22:19:53 +00:00
23819961e6
Populate compliance address based on site location 2026-04-22 21:22:33 +00:00
a8819c907e
Add concern page to mailer compliance flow 2026-04-22 21:22:03 +00:00
b5923137a7
Set organization (district) for compliance reports from mailer 2026-04-22 19:54:06 +00:00
78458760ec
Navigate to cell on aggregate map click 2026-04-22 15:46:02 +00:00
1286d0ea2a
Fix organization ID for aggregate map 2026-04-22 15:40:42 +00:00
2fbcb9f918
Add next value to signin page and actually use it 2026-04-22 15:30:24 +00:00
5cdbc4eb53
Fix links in the compliance process 2026-04-22 14:49:04 +00:00
b4527fba8b
Develop patterns for creating links outside router 2026-04-22 14:33:56 +00:00
bcd51cf5cf
Fix compliance query again.
Blarg.
2026-04-22 00:22:51 +00:00
8a9a3e8c0c
Update pnpm deps hash from sentry update 2026-04-22 00:05:27 +00:00
986d12eab2
Initialize sentry after getting API status 2026-04-21 23:58:04 +00:00
839abcbd28
Mave frontend data to base api root
Because many times we don't have a session
2026-04-21 23:53:42 +00:00
544ac78a3b
Add frontend configuration to session for env, sentry, version 2026-04-21 23:44:59 +00:00
8d37e8fab5
Fix compliance query
I can't use this until I fix some bugs in bob :(
2026-04-21 23:36:29 +00:00
2b30411c1b
Add sentry integration with Vue frontend 2026-04-21 23:35:59 +00:00
baaa3bff5b
Make request parser handle form-encoded content
This fixes a new signin bug
2026-04-21 22:48:31 +00:00
0ce3420792
Save all lob events to the database
They're pretty raw, but this will help us to understand what we can
collect
2026-04-21 22:24:12 +00:00
4db1a6f678
Add support for data fields for letter.created 2026-04-21 22:11:53 +00:00
f24104dc94
Update lob hook to handle both address created and letter billed payloads
Seems we'll have a lot of optional values
2026-04-21 22:00:09 +00:00
ee9a355613
Serialize nil slices as empty slices 2026-04-21 21:55:48 +00:00
810a13cee0
Add initial lob hook receiver 2026-04-21 21:55:37 +00:00
fe2041f22b
Add an evidence field to compliance reports
This allows us to show a page with information about what the district
is concerned about when asking the user to fill a report.
2026-04-21 21:35:40 +00:00
a0ac5c0674
Add debug logs around authentication
Trying to troubleshoot our redirection logic after signin
2026-04-21 19:40:00 +00:00
bcc5151116
Don't compliance report on root Compliance page
We're now doing that through our two entrypoint pages.
2026-04-21 19:39:18 +00:00
8fd86d478c
Update mailer page to show actual data 2026-04-21 19:38:46 +00:00
0b005c3e76
Add debug logs around exiting goroutines
I'm debugging our clean shutdown
2026-04-21 19:37:58 +00:00
4a214b099e
Disallow login or sessions from inactive users 2026-04-21 19:37:26 +00:00
eb27af7d90
Add mailer API and initial mailer view 2026-04-21 19:19:59 +00:00
0d8d7f3aeb
Add link for reviewing mailers 2026-04-21 15:01:46 +00:00
80031c1d1a
Make response to compliance report creation consistent 2026-04-21 15:01:01 +00:00
bcea3c6bdf
Gracefully exit listenForJobs when context ends 2026-04-21 14:59:52 +00:00
bad50a8772
Clean up compliance report creators and share UI 2026-04-21 14:45:11 +00:00
bd3e42f83e
Use the same create logic for Mailer report creation 2026-04-21 14:41:06 +00:00
f927b0a911
Split out ComplianceDistrict view for creating new compliance reports
The idea here is that we'll make compliance reports two different ways,
The first is if the user navigates to /district/:slug/compliance, the
second if they open a QR code from a mailer. In both cases we create the
report then feed them into a flow for updating the data on that report.
2026-04-21 14:35:13 +00:00
8eae73eefb
Add initial compliance mailer page
It loads at this point. Woot.
2026-04-20 23:16:57 +00:00
5d510915d2
Add version to frontend connection 2026-04-20 22:42:21 +00:00
e2d4f917a0
Add script for running output of "nix build" 2026-04-20 22:41:55 +00:00
2a3dbbdad3
Show login error on failure 2026-04-20 22:34:39 +00:00
7a6cffa74c
Cleanup unused variables 2026-04-20 22:34:24 +00:00
e929118349
Reduce log spam on user login error 2026-04-20 22:34:10 +00:00
aae0d1ed74
Log version on startup 2026-04-20 22:33:56 +00:00
8387cf667b
Add company filter to Lob list addresses
...even though I never made it actually work.
2026-04-20 22:33:20 +00:00
ffd424df12
Save the organization with the compliance report on creation
This avoids the problem of having to assign the compliance report later
when we get location data and image data.
2026-04-20 16:21:08 +00:00
0b32492fd6
Add version information to build output 2026-04-20 01:58:44 +00:00
ade629ecf5
Rework background jobs to make transactions much shorter
I ended up with minutes-long open transactions in the database in prod
which was causing outtages. This is because I thought transactions were
basically free, which is a terrible thing to think. Instead we'll just
open them when we need them.
2026-04-17 22:53:23 +00:00
55cb4ca962
Track the site with the URL 2026-04-17 22:04:24 +00:00
efd6f59fca
Populate ComplianceReportRequest on site review page 2026-04-17 21:40:04 +00:00
a6ca30fdb1
Add application name to transaction
Trying to find what's getting locked
2026-04-17 21:27:30 +00:00
3196b73a80
fix setting up compliance map 2026-04-17 20:58:21 +00:00
5a865cc5e1
arcgis-go bump 2026-04-17 20:55:11 +00:00
abbe80b1f0
Don't fail to process background jobs because one failed 2026-04-17 20:51:24 +00:00
ac552be7e7
Send compliance report data with lead data 2026-04-17 20:51:07 +00:00
cedbb3372e
Try to capture more data on the failure to create address with Lob 2026-04-17 20:50:43 +00:00
0420b777c9
Remove chatty log 2026-04-17 20:50:30 +00:00
83bf3023de
Update vendor has for arcgis-go update 2026-04-17 20:26:55 +00:00
0e777568fb
Add sublogging for job work for debugging 2026-04-17 20:25:25 +00:00
75e9d5a621
Bump arcgis-go version to 0.0.12 2026-04-17 20:25:06 +00:00
a2cdbc26bd
Allow signin with next parameter 2026-04-17 19:44:08 +00:00
be9065354d
Detect when we fail to get tile service 2026-04-17 19:43:57 +00:00
fa675f293d
Add initial work on getting compliance data for leads 2026-04-17 19:43:40 +00:00
b7d26d5ad7
Only log every route if we have VERBOSE enabled 2026-04-17 19:39:10 +00:00
21587493c0
Stop swamping the server on reboot 2026-04-17 18:36:05 +00:00
4625dd39d0
Default sort sites by created date 2026-04-17 18:25:04 +00:00
fd662721bb
Fix non-rolled-back transactions 2026-04-17 18:19:13 +00:00
4a8c0d2e60
defer rollback rather than guard returns
I'm trying to make sure we close transactions on the database
2026-04-17 18:00:26 +00:00
c938cb231e
Add org name and user name to dashboard 2026-04-17 17:51:02 +00:00
ba8c0016ac
Add sigup page...again
Had it previously, but broke it for the single-page app migration.
2026-04-17 17:48:18 +00:00
b6e1bffd79
Add support for satellite tiles, with caching 2026-04-17 17:47:38 +00:00
61351dabf1
Update UI when a compliance letter is sent 2026-04-17 15:20:17 +00:00
efa01cffc2
Move session management into session store
Trying to get rid of the redirect to signin on any page refresh
2026-04-17 14:52:02 +00:00
bf156eaf7f
Slim down chrome requirements for the server 2026-04-17 14:51:24 +00:00
cb92b845e6
Show sites with leads more clearly 2026-04-17 03:10:08 +00:00
b85deb229f
Improve padding for multiple marker points 2026-04-17 03:07:16 +00:00
bff81eb6e3
Add basic lead type 2026-04-17 02:59:22 +00:00
617631063f
Add quick'n'dirty interface for leads and features 2026-04-17 02:59:01 +00:00
1f8e6b698f
Allow for a lot more sites, and for scrolling 2026-04-17 02:40:05 +00:00
09fe773987
Use the correct URL for generating pdfs 2026-04-17 02:31:57 +00:00
273e2b7b32
Ignore new lob test utils 2026-04-17 02:23:28 +00:00
dc3cce0b8a
Switch to multipart upload of PDF to lob, move backend to using that. 2026-04-17 02:22:05 +00:00
2c0aa980e7
Working creation of a letter
I had to use an address ID for 'to' and 'from' and then did really
weak-sauce inline HTML.
2026-04-17 01:00:16 +00:00
3ab0a00959
Working path to create addresses in lob 2026-04-17 00:23:34 +00:00
2ddf015a68
Skip pools without an address 2026-04-17 00:11:47 +00:00
b7eff027e7
Parse out lob's address format 2026-04-17 00:02:10 +00:00
fde9539191
Don't attempt to find the parcel if our address ID is nil
That's because the address doesn't have location data, so we can't find
a parcel.
2026-04-16 23:54:18 +00:00
6945b9f9ed
Drop to a single worker in the geocode pool
I'm sharing transactions incorrectly, and until I fix that I need
correctness, not speed.
2026-04-16 23:50:19 +00:00
5100c8f0be
Stop redirecting all loads to the dash page 2026-04-16 22:51:15 +00:00
1aba99f732
Remove chatty debug logs 2026-04-16 22:51:00 +00:00
97ec0667a5
Initial success talking to lob directly with my own client 2026-04-16 22:41:43 +00:00
c0935c848b
Default required fields to empty strings
So the insertion doesn't fail
2026-04-16 22:32:48 +00:00
a2271a2ce8
Add more debug info about user login 2026-04-16 22:08:28 +00:00
83c013785f
Fix swizzled email args 2026-04-16 21:58:41 +00:00
e464a9fcdb
Longer timeout on axios client
We're hitting the 10sec timeout trying to do login
2026-04-16 21:24:45 +00:00
59bf360937
Try to get more debug info on lob 420 error 2026-04-16 21:00:24 +00:00
a33056039a
Update vendor hash 2026-04-16 21:00:13 +00:00
f1890332ae
Get proper cached images from the tile server 2026-04-16 20:51:17 +00:00
b3a89d9c68
More protections for oauth being expired 2026-04-16 20:43:40 +00:00
a922196f20
Add mailer file utilities 2026-04-16 20:41:13 +00:00
89a2cb30e6
Save the type from the database on feature 2026-04-16 20:40:55 +00:00
1bc452bc09
Avoid crashing when getting Fieldseeker client 2026-04-16 20:40:37 +00:00
ee2254281c
Fix erasing feature locations 2026-04-16 20:39:30 +00:00
59755a0b42
ignore new tile-raster helper command 2026-04-16 20:38:19 +00:00
d03c12ffb6
Add working ability to get stadia tiles directly 2026-04-16 20:37:49 +00:00
163b0f9edc
Destroy the session on signout
Kill it with fire
2026-04-16 19:50:23 +00:00
a6f9396760
Add first draft of mailer integration
This adds a bunch of stuff, including setting the organization's Lob
sender address ID, inserting mailer/compliance_report relationships,
adding external id from Lob (or maybe some other provider) and
attempting to load up the pool feature for a site.
2026-04-16 19:49:18 +00:00
84da2bdc7d
Show full address on site page 2026-04-16 18:55:47 +00:00
ed6dde2f0a
Fix navigation after login 2026-04-16 18:50:34 +00:00
3379251ccb
Fix missing AddressAndMapLocator import for RMO water 2026-04-16 18:03:40 +00:00
7483a6a695
Redirect to signin on session failure 2026-04-16 17:30:54 +00:00
d047c460ed
Add missing files in last two commits 2026-04-16 17:15:44 +00:00
81e057b900
Add initial work for backgrounding mailer job 2026-04-16 17:15:20 +00:00
b6d1bd9ee2
Create sign-in and sign-out workflow in SPA 2026-04-16 17:14:57 +00:00
08a1b5b81d
Don't send www-authenticate to well-behaved browsers
Had to make a special case for EventSource on the browser via the
accepts header. This prevents the browser from doing a login window so
we can show them the nice login page.
2026-04-16 16:01:45 +00:00
7b95cfe833
Actually commit the transaction 2026-04-16 10:49:06 +00:00
6b90edf053
Fix creation of signal for mailer 2026-04-16 10:47:13 +00:00
f444bf39fb
Update hashes for deployment 2026-04-16 10:46:55 +00:00
2ea47f03f4
Start wiring together request for a mailer to database 2026-04-16 10:15:28 +00:00
74e24b7de3
Add feature to site data 2026-04-16 09:04:25 +00:00
5a35c1d1f8
Show parcel information on site page 2026-04-16 08:26:48 +00:00
5d06afbecc
Add maps for site review 2026-04-16 08:09:03 +00:00
8514ec36d5
Allow for selecting sites 2026-04-16 07:55:08 +00:00
35ab261ee8
Add missing site store 2026-04-16 07:43:53 +00:00
838e24bbed
Stop losing webGL context on review complete
It makes things *much* faster
2026-04-16 07:43:17 +00:00
84604dfdc8
Fix reference to site owner 2026-04-16 07:25:06 +00:00
d86ef13345
Flesh out the start of the site list 2026-04-16 07:20:53 +00:00
b9c257a635
Add site contact information 2026-04-16 07:12:34 +00:00
671397ba81
Make a home for reviewing sites 2026-04-16 07:03:45 +00:00
c4c22f6733
Start to populate site information in review task 2026-04-16 06:58:05 +00:00
03dccb638a
Start adding support for lob 2026-04-16 06:57:20 +00:00
e3f9a19b84
Allow deselecting review tasks
Makes it so I can test the map losing gl context
2026-04-16 05:36:54 +00:00
262aa009c2
Avoid losing map context when selecting tasks 2026-04-16 05:36:28 +00:00
74e0630a41
Revert proxied tile to not use layer control
It's disabling the important layer.
2026-04-16 05:14:27 +00:00
29c3b267d9
Add pieces of initial site review page 2026-04-16 04:48:07 +00:00
e1f3c93a1d
Make it possible to click on either map to choose a pool 2026-04-16 04:47:41 +00:00
259960cf45
Show row number in UI for pool uploads 2026-04-16 04:21:32 +00:00
d395699dc4
New algorithm for detecting multiple conflicting features 2026-04-16 04:21:11 +00:00
f490e4a1a4
Avoid duplicate rownumber calculation
We have the row number saved on the pool object itself, which is how we
gather errors against rows.
2026-04-16 04:20:28 +00:00
d352b0d932
Get pool rows by line number so they stay in order
Because otherwise the errors don't line up correctly.
2026-04-16 04:01:44 +00:00
a9077b6c36
Fix framing locations on the map display 2026-04-16 03:46:56 +00:00
5f68eb453f
Add placeholder for when we fail to extract address data from a feature
This bit me recently when getting the number from an address
2026-04-16 03:29:08 +00:00
5e0981e2a2
fix bad copy paste on address field 2026-04-16 03:16:57 +00:00
b2d8e3ba27
Move address list func to types so it can be shared with csv
And stop double-geocoding all the rows.
2026-04-16 03:06:18 +00:00
5bf93c3dfd
Fix erroneosly showing error marking on good rows 2026-04-16 03:05:31 +00:00
b2c5bb6735
Show map on upload detail page
Bunch of stuff still doesn't work right.
2026-04-16 02:48:12 +00:00
171672ee33
Fix minor error on upload detail page rendering 2026-04-16 02:47:29 +00:00
aa5a35b15f
Create option to use satellite imagery
Useful for looking at pools
2026-04-16 02:47:11 +00:00
82dd5e8683
More debugging for CSV import 2026-04-16 02:46:55 +00:00
f5ac7bb4ee
Set address form pool rows using address model if possible 2026-04-16 02:46:24 +00:00
e894ae28dc
Add initial site list resource 2026-04-16 02:45:48 +00:00
d55a7ec5af
Add initial sort-of-working layer selector 2026-04-16 01:47:33 +00:00
0e165b57d0
Default header to tag type 2026-04-16 00:18:42 +00:00
dfe7d3650f
Default header type to unknown
This is a subtle bug from the zero value of a header enum that's causing
overwriting in pool uploads
2026-04-16 00:14:35 +00:00
b6951d64d4
Remove api_key from URL to stadia cache
It's redundant and a security risk
2026-04-16 00:09:18 +00:00
ee38d0d2b6
Add fake operations map component 2026-04-15 23:45:05 +00:00
ac27c60e0c
Save address IDs when doing pool geocoding 2026-04-15 20:29:42 +00:00
6a8ae6d81a
Exit the geocode job if we hit an error 2026-04-15 19:31:55 +00:00
87c802fa90
Fix relationship for looking up whether the pool is in the district 2026-04-15 19:31:32 +00:00
b08582224a
Add missing required state header
I was incorrectly mapping "city" to "region" previously. A 'region'
actually is closer to a state. We need a locality, which is closer to a
city.
2026-04-15 19:30:50 +00:00
66d35428fa
Add error display to file upload 2026-04-15 19:02:25 +00:00
344f4bcaa5
Fix redirect after discard 2026-04-15 18:34:43 +00:00
ac65129ba6
Fix ability to discard upload 2026-04-15 18:32:28 +00:00
322be2fe40
Fix redirect on CSV upload 2026-04-15 18:32:19 +00:00
1097004245
Add custom pool upload page 2026-04-15 18:25:38 +00:00
f4d0ce015d
Factor upload requirements out into parent component 2026-04-15 17:36:33 +00:00
adcff5c5c8
Split upload requirements table into its own component 2026-04-15 17:27:22 +00:00
388801fd09
Fix upload links for pools 2026-04-15 17:24:34 +00:00
cd98751667
Avoid unloading the maps on pool review
It's expensive and slow
2026-04-15 17:23:26 +00:00
b0e2e97f09
Make changes actually reflect the changes 2026-04-15 16:59:53 +00:00
239340a7a9
Allow for selecting new location for a pool 2026-04-15 16:50:43 +00:00
31d8d2d0d5
Populate pool review form in the parent view
This will allow us to change it.
2026-04-15 16:42:16 +00:00
05ec6798ac
Get markers to show up on maps in pool review page 2026-04-15 16:22:08 +00:00
5549f9d79f
Wire up logic for completing and discarding a review 2026-04-15 14:11:48 +00:00
0fbf891c23
Show filler until a review task is selected 2026-04-15 14:07:43 +00:00
9ea99c92f9
Get overview map working on review details page 2026-04-15 00:12:19 +00:00
8ebcff7390
Fix oauth callback for arcgis to be under oauth prefix
That way it gets through the Vite proxy.
2026-04-14 23:43:53 +00:00
659df00cc9
Allow refreshing the oauth token in the frontend 2026-04-14 23:41:40 +00:00
5451c297c2
Harmonize review page properties between front and back ends 2026-04-14 23:29:29 +00:00
b09725726c
Create API for service requests list 2026-04-14 23:06:50 +00:00
4a440e3022
Add a resource for getting service requests 2026-04-14 19:59:32 +00:00
28ec1c3d67
Get latest syncs from the API 2026-04-14 19:21:51 +00:00
347e8dcb86
Update geocode store to use new naming pattern 2026-04-14 18:40:54 +00:00
67dcb87b81
Add missing water report detail component 2026-04-14 16:31:08 +00:00
b849eec7ea
Restore status page for standing water report 2026-04-14 16:28:58 +00:00
ebbd79ed7e
Fix status display of type-specific report details 2026-04-14 16:20:44 +00:00
fe41df3e16
Make publicreport by ID base redirect to detailed information 2026-04-14 16:07:17 +00:00
4a28a16639
Show address location, not reporter location, in the status page 2026-04-14 15:46:52 +00:00
59e58840c9
Fix address lat/lng location names, populate in report response 2026-04-14 15:43:49 +00:00
7e2a22c58c
Mave report ByID to their own resources 2026-04-14 15:31:10 +00:00
9b1de15373
Fix public report ID for water reports 2026-04-14 15:28:41 +00:00
c84a2ef42b
Properly set public ID 2026-04-14 15:24:21 +00:00
5527731e83
Add can SMS question, fix error handling of client ID 2026-04-14 15:15:19 +00:00
84db38c985
Include client ID in nuisance and water reports 2026-04-14 14:50:28 +00:00
a23866619d
Start saving client ID on compliance reports 2026-04-14 14:38:22 +00:00
7545b2e4ef
Update the report after uploading images 2026-04-14 02:44:10 +00:00
5448702a7d
Fully populate report after PUT
Otherwise we miss stuff like the number of images
2026-04-14 02:38:36 +00:00
2408bcbeff
Save public_id after creating report so we see it in the UI 2026-04-14 02:35:41 +00:00
e707d91e7f
Create images on the correct URI 2026-04-14 02:35:04 +00:00
e2b6bc6502
Use JSON POST for creating the compliance report 2026-04-14 02:33:39 +00:00
b805374a6c
Fix not panning to location when map is loaded 2026-04-14 02:02:34 +00:00
6e1a5b4348
Avoid jumping to empty camera location 2026-04-14 01:45:04 +00:00
cadf6afb5f
Use embedded address location rather than external location on geocode 2026-04-14 01:42:53 +00:00
3c62fe2ca1
Be consistent about using report.public_id over report.id 2026-04-14 01:26:23 +00:00
02139450c6
Always set reporter phone can SMS 2026-04-14 01:21:50 +00:00
3ae72c8944
Check for address before inserting a new one. 2026-04-14 01:20:52 +00:00
a189348b36
Remove existing report URI when submission completes 2026-04-13 23:51:43 +00:00
8f494991e2
Make final pages show real data 2026-04-13 22:46:43 +00:00
f74d2c3ca1
Show phone and email as present if they are only on the server 2026-04-13 22:35:00 +00:00
c0389fa4b1
Stop overwriting the address by ID
We can pull this in the single query we do to the database instead
2026-04-13 22:34:36 +00:00
9c557a0391
Make it possible to save SMS support status on phone record 2026-04-13 22:23:29 +00:00
96878f24de
Get contact information to save in compliance flow 2026-04-13 21:45:29 +00:00
083c4ddae9
Save access information to database 2026-04-13 20:42:03 +00:00
ba76c8b1db
Return full compliance report on PUT 2026-04-13 19:32:22 +00:00
9bca15ae7e
Use the right URI for compliance reports 2026-04-13 19:25:54 +00:00
ffedae0373
Update address foreign key when updating the address 2026-04-13 19:22:41 +00:00
dcab2e1f8f
Fix failing to find matching address with publicreport 2026-04-13 17:19:20 +00:00
92f4282674
Track map loading, frame markers when map is loaded
This prevents missing the marker framing because we are still loading
the map.
2026-04-13 16:59:59 +00:00
8baa056fab
Return a compliance URI on creation 2026-04-13 16:59:34 +00:00
5011f4c137
Add raw address value to public report response 2026-04-13 16:59:20 +00:00
1a031f16bd
Update event types to include compliance reports 2026-04-13 16:59:11 +00:00
5db4c05544
Add proper compliance report type
Can't believe I missed this.
2026-04-13 16:42:29 +00:00
0f94292ab7
Fix zoom in when we load existing report data 2026-04-13 15:22:50 +00:00
b701771dfb
Remove district loading debug log 2026-04-13 15:17:57 +00:00
447ea18d95
Bit of type cleanup when debugging 2026-04-13 15:17:57 +00:00
756cc0d266
Add properties to update compliance permission access 2026-04-13 15:15:33 +00:00
0297114faf
Remove unnecessary null coalesce 2026-04-13 15:14:48 +00:00
dddeafe6cd
Fix query for address IDs 2026-04-13 15:13:59 +00:00
6f5b8f5575
Add implementation of insertAddresses 2026-04-12 19:42:37 +00:00
9ba99d5ceb
Remove now-empty report address fields
We'll instead create address rows and reference those
2026-04-12 18:33:41 +00:00
5306f8ba62
Populate nuisance and water public reports by ID 2026-04-12 18:02:42 +00:00
ae10e4fee8
Initial pattern for populating different report types 2026-04-12 17:53:25 +00:00
c8f74d3c26
Consistently use 'public' prefix for reports 2026-04-12 17:07:14 +00:00
a3c340f787
Split public report URIs by type
This allows us to have different signatures for the different types
2026-04-12 17:01:30 +00:00
875298fe88
Show a warning if they will replace the images on the report 2026-04-12 16:44:20 +00:00
ab47259534
Fix sending empty longitude when creating the initial report 2026-04-12 16:29:37 +00:00
60eb6b9bbf
Use class heirarchy for different report types. 2026-04-10 23:57:47 +00:00
4735734404
Helper functions for parsing geocode data 2026-04-10 22:34:34 +00:00
4060e7ddcd
Upload images on compliance report 2026-04-10 22:34:14 +00:00
730f40956f
Store addresses on every geocode 2026-04-10 22:32:40 +00:00
e04b86218d
Fix bad compliance report PUT
Avoid attempting to PUT the location when we don't have a report URI
2026-04-10 20:30:22 +00:00
12aedaf543
Update the address when provided on a report 2026-04-10 20:30:22 +00:00
bac55774f8
Switch address to contain an embedded location, start saving compliance 2026-04-10 16:59:29 +00:00
14c0d453e9
Add loading indicator when checking for previous report data 2026-04-10 15:38:31 +00:00
b23fc6edc5
Fix dodgy creation of compliance report in database 2026-04-10 15:38:05 +00:00
c48aebcb0b
Set initial camera based on location in compliance 2026-04-10 14:20:04 +00:00
97acdb0e2c
Prevent the lock button from floating over address suggestion 2026-04-10 13:47:00 +00:00
f969f262b8
All spaces at the end of address input
Othewise you can't type, it sucks.
2026-04-10 13:44:58 +00:00
ae50a1abd8
Remove the remains of the old bundle paths 2026-04-10 02:59:55 +00:00
553b65556a
Begin work on saving compliance report 2026-04-10 00:56:51 +00:00
3ad95e1365
Bind contact info to compliance model 2026-04-09 22:48:49 +00:00
86ab67e70b
Bind access information to the compliance model 2026-04-09 22:42:47 +00:00
3bde7a9cac
Save image data on the compliance model 2026-04-09 22:33:45 +00:00
a4a9662c94
Make submit page read from model values 2026-04-09 22:29:42 +00:00
79a56c2d20
Don't show default locator value 2026-04-09 22:29:26 +00:00
d3662b8240
Preserve the locator model
This makes it possible to move back-and-forth in the compliance process
and still retain data.
2026-04-09 22:22:27 +00:00
dbc5db9727
Link up data to final page. 2026-04-09 20:55:30 +00:00
5b5a63114c
Make the permission property an enum 2026-04-09 19:55:08 +00:00
a6912929a7
Establish basic pattern for compliance data flow
We can get location, images, and string data. It's the trifecta.
2026-04-09 17:35:00 +00:00
8d6976c770
Actually go to the next step when we get the location 2026-04-09 17:24:50 +00:00
9dccd21cee
RMO frontend checkpoint
* Create a nwe AddressAndMapLocator which abstracts out the behavior of
   selecting a location
 * Fix the overlay causing render errors on the MapLocator by getting
   rid of the overlay and just using a lock indicator
 * Fix MapLocator zooming in to the wrong place by not framing the
   markers
 * Remove Latlng from platform and just use Location with optional
   accuracy
 * Use nested types with form-encoded POST
 * Fix styles on water report page
2026-04-09 17:21:35 +00:00
cb9e5146bf
Fix display of report ID and status on the by-id page. 2026-04-09 13:48:48 +00:00
882636de8f
Add nuisance report detail to status by ID page 2026-04-09 01:15:13 +00:00
531f3282d9
Move bounds to API types 2026-04-09 01:02:25 +00:00
f88ca57d97
Migrate existing ts types from the API into the API module
This makes it possible to start hydrating the types into valid data
types like Dates which means I can get type safety guarantees when
displaying information.
2026-04-09 00:25:21 +00:00
b2c24a0438
Show nuisance report status 2026-04-08 23:37:00 +00:00
37ce3183ca
Add beginnings of status page 2026-04-08 22:54:20 +00:00
2c0bfb9904
Update nuisance submission to go to submitted page 2026-04-08 17:51:41 +00:00
c41154a200
Actually serialize errors on bad JSON POST response 2026-04-08 17:27:51 +00:00
7f90391ecd
Remove old generated JS/CSS paths
We generate a whole file now.
2026-04-08 15:01:02 +00:00
1a7a2b13aa
Navigate to report complete when report is submitted 2026-04-08 14:59:30 +00:00
6c79b8a85e
Add address GID to public report
This is _way_ better than trying to re-transmit structured address data
to the backend via strings
2026-04-08 14:40:27 +00:00
765b8fbef7
Better overlay logic for clicking on map controls 2026-04-08 14:25:47 +00:00
8e536d1d2f
Add map overlay for phone interactions 2026-04-08 14:11:30 +00:00
68315a3fb2
Fix button icons on complete page 2026-04-07 16:26:34 +00:00
6fb5a7f971
Fix icons on RMO, add phone data to district 2026-04-07 16:25:14 +00:00
cc7ce44f47
Finish porting styles of compliance flow 2026-04-07 16:05:30 +00:00
53bfbbc5ef
comment-out useHead 2026-04-07 16:05:04 +00:00
9601b88b41
Fix breaking the API routing
Ordering matters when you have a catch-all for the SPA
2026-04-07 15:42:48 +00:00
f22ddd0405
Remove attempt at dynamically generating SPA content
We just do it with vite now.
2026-04-07 15:09:59 +00:00
1a53d5338f
Work out the rest of the static site deployment 2026-04-07 14:56:31 +00:00
e7c33d7e10
Get farther in producing a build 2026-04-07 02:54:48 +00:00
d52101d25b
Begin work on nix deployment logic for vite 2026-04-07 02:02:45 +00:00
6f677b5638
Add the full compliance mocks 2026-04-07 00:53:44 +00:00
4faa7fa8c0
Figure out router pattern for compliance steps 2026-04-07 00:04:40 +00:00
20614acb86
Add initial compliance intro page 2026-04-06 22:38:17 +00:00
c393f6fd81
Add cache for all stadia requests 2026-04-06 22:36:25 +00:00
9ef6aaa406
Remove direct calls to stadia API from geocoding 2026-04-06 16:59:19 +00:00
43dce16fbd
Add APIs for geocoding and reverse-geocoding 2026-04-06 16:59:18 +00:00
437f87013a
Add information on resetting password 2026-04-06 15:58:04 +00:00
380b41f695
Add ability to query for a place by gid(s) 2026-04-06 15:36:17 +00:00
2d5dca3fb5
Add proxied autocomplete for Stadia
This allows me to make the format consistent and to cache the
intermediate results, which is useful for speed and testing
2026-04-05 21:57:30 +00:00
b6cfbee102
Add geocoding logic/store 2026-04-05 03:47:22 +00:00
5681ff2283
Fix render crash 2026-04-05 03:09:10 +00:00
332e64c9ab
Add basic location store for getting geoposition 2026-04-04 02:32:09 +00:00
beb6d9d066
Better zoom to location on address selection 2026-04-03 23:11:39 +00:00
e56e83161b
Include address information on nuisance form upload 2026-04-03 23:04:04 +00:00
e08f614d11
Make the locator model a camera, not just a location
That means we can track zoom
2026-04-03 22:42:50 +00:00
10e368c403
Get initial nuisance and water resources working
This is a straight port of the form-encoded POST submission logic.

It is missing a bunch of data.
2026-04-03 22:04:22 +00:00
597aedc2af
Fix show more questions behavior 2026-04-03 20:46:02 +00:00
07e48aa071
Zoom when an address is provided or the map is clicked 2026-04-03 20:29:30 +00:00
c5c78a2b84
Add initial ImageUpload component 2026-04-03 20:15:02 +00:00
9104e2f7c3
Start map with default framing on nuisance page 2026-04-03 20:01:23 +00:00
27fd1faa9c
Get clean-building locator map 2026-04-03 19:45:12 +00:00
6203e3da75
Add nuisance style, fix header on non-home district pages 2026-04-03 19:07:20 +00:00
b6037d7525
Add address suggestion component 2026-04-03 19:02:20 +00:00
51fe851c5a
Add district-styled pages for all 3 main RMO pages 2026-04-03 18:50:23 +00:00
c0e414bdc3
Add status page to RMO 2026-04-03 18:29:29 +00:00
5842b6251d
Add district header on root page 2026-04-03 18:28:41 +00:00
bfecae7d61
Add district resource and an API to RMO
We're going to need an API for the single-page frontend
2026-04-03 18:17:19 +00:00
4f9617aa2f
Add RMO water page, start district layout 2026-04-03 16:37:09 +00:00
fd7607f5b7
Add nuisance view for RMO 2026-04-03 16:08:57 +00:00
64a8de7a32
Add custom icons to RMO system 2026-04-03 16:00:50 +00:00
4a9d6e0db6
Port root RMO with style to main page 2026-04-03 15:58:50 +00:00
4d718f9a12
Add router and basic home view 2026-04-03 15:43:44 +00:00
2342a99405
Save experiment in postgred integration 2026-04-03 15:25:15 +00:00
4f6369fa27
Fix static content for RMO 2026-04-03 15:24:19 +00:00
e8db3de122
Remove old index page 2026-04-03 15:19:54 +00:00
b658e28f2e
Get static content showing on sync 2026-04-03 15:15:47 +00:00
b919472f42
Make RMO and Sync run in parallel and use the same sources 2026-04-03 15:09:53 +00:00
d7d6888f63
Initial commit of some work creating RMO single-page app
Doesn't work yet, but they both start, so checkpoint.
2026-04-03 15:02:37 +00:00
54e77f72f4
Add completion page 2026-04-03 14:34:46 +00:00
3e0003095b
Add process and submission mocks 2026-04-03 03:57:43 +00:00
095ab828b6
Add contact page in compliance flow 2026-04-03 03:43:55 +00:00
06345099eb
Add permission mock in compliance flow 2026-04-03 03:41:26 +00:00
5cabea8577
Add evidence mock 2026-04-03 03:35:45 +00:00
42bcdb8af8
Add mock for concern page 2026-04-03 03:26:52 +00:00
bfe2b88622
Add compliance address mock 2026-04-03 03:22:09 +00:00
377683c4e3
Update compliance landing page
Show district logo, phone number, etc.
2026-04-03 00:18:15 +00:00
457f123f69
Add simple compliance landing page 2026-04-03 00:12:52 +00:00
4b87c74f41
Make impersonation ending work, fix frontend events 2026-04-02 21:31:31 +00:00
522c5785a2
Create button for ending impersonation 2026-04-02 19:36:49 +00:00
76c395d613
Add display in sidebar for impersonation 2026-04-02 17:39:16 +00:00
51811132a4
Add avatar display to user selector 2026-04-02 15:39:52 +00:00
ea231fb0cc
Avoid failure when cloning proxied object 2026-04-02 15:32:48 +00:00
9574ed4812
Fix warning on user edit component 2026-04-02 15:26:03 +00:00
af2299f417
Fix saving of tags on users 2026-04-02 15:25:51 +00:00
945b482b00
Fix saving of tags on users 2026-04-02 15:10:51 +00:00
f9934095b3
Fix reference to avatar URI 2026-04-02 15:09:59 +00:00
aa02d2e729
Fix being able to set the role 2026-04-02 14:30:07 +00:00
ee76dddf2f
Add some missing files from previous commits 2026-04-02 14:23:16 +00:00
fc56c1406a
Make it possible to change more user fields 2026-04-02 14:22:45 +00:00
7ee70b24ee
Fix user data displays 2026-04-02 14:03:07 +00:00
3745231f51
Structure PUT by using omit.Value 2026-04-02 13:28:18 +00:00
353a3ea442
Use the correct scheme for URIs 2026-04-02 01:18:25 +00:00
124d1b7078
Show the avatar on the user edit page 2026-04-02 01:11:51 +00:00
42d111aac9
Add separate session endpoint for additional non-user data
This is conceptually much cleaner that encumbering the user object.
2026-04-02 01:07:55 +00:00
00ebc27069
Add reverse parsing of a URI.
Yay. I did it. All the work is worth it now.
2026-04-01 22:01:31 +00:00
4145944b1b
Allow arbitrary responses from form-encoded POST
Useful for returning full objects
2026-04-01 21:34:17 +00:00
a89a4fbec5
Add avatar resource 2026-04-01 21:23:28 +00:00
0a7a2512d4
Properly set Avatar value to null 2026-04-01 20:35:00 +00:00
6fbde6389d
Start creating user resources without ID. 2026-04-01 20:22:15 +00:00
a656d45a6d
Move QueryParams to resource module 2026-04-01 18:23:43 +00:00
ab519020fc
Swap out the rest of chi
We're now chi-free.

Not bug-free.
2026-04-01 16:57:33 +00:00
6c311c76e3
Initial draft of shifting from chi to gorilla/mux 2026-04-01 16:19:11 +00:00
5172400803
Begin switch to gorilla/mux
I'm realizing with this code that I'm going to have a problem if I want
to do HATEOAS-style APIs. chi just doesn't do resource-oriented API
design, and I'd have to build a lot of stuff myself.

I'm in the middle of swapping out the UI. Now is the time to make the
switch.
2026-04-01 15:32:27 +00:00
c253e655b1
Add avatar placeholer when avatar is empty 2026-04-01 14:48:31 +00:00
0ecf9c1be1
Populate user selector 2026-03-31 23:34:03 +00:00
05a7bbb4e3
Show empty aggregation map without service area 2026-03-31 23:29:37 +00:00
7f8491a1c2
Add test mailer 2 2026-03-31 20:05:35 +00:00
7f72e82ceb
Fix favicon on sync 2026-03-31 19:18:33 +00:00
af136f324d
Break sudo page into components
Makes it easier to fix the overall layout, which I've done.
2026-03-31 17:34:37 +00:00
4f96f35d9a
Add mode 1 mailer for testing. 2026-03-31 17:34:08 +00:00
7b3c1f2b54
Add initial implementation of user selector on sudo 2026-03-31 15:10:32 +00:00
21b7b68f50
Get new frontend to type check clean
Epic undertaking.
2026-03-31 14:52:53 +00:00
6f9a511874
WIP of user avatar work
Switching from laptop
2026-03-29 17:09:01 -07:00
ad90f9c95e
Create API for adding an avatar to a user 2026-03-28 18:55:13 -07:00
da7549eeda
Show actual user data on the edit page. 2026-03-28 18:06:14 -07:00
92ed974e4b
Use overlay buttons to change avatar 2026-03-28 17:23:09 -07:00
15371ec064
Add basic user edit page 2026-03-28 16:31:29 -07:00
4bfaaa72ce
Add URI to user resource 2026-03-28 15:13:11 -07:00
e59794f5e0
Query for users to populate the users page 2026-03-28 14:45:49 -07:00
1f9f1ae166
Fix router links on configuration page 2026-03-28 13:02:04 -07:00
9a9371301c
Get review detail UI to show without crashing
It doesn't fully work yet though.
2026-03-28 12:35:12 -07:00
9921618c12
Get to where we can display something on pool review 2026-03-28 09:14:09 -07:00
33399b5e2a
Fix links on review page 2026-03-28 06:50:25 -07:00
da14410fc7
Update sidebar links to new format 2026-03-28 06:47:20 -07:00
6f8c012394
fix filename and status displays 2026-03-28 06:45:03 -07:00
daf921accf
Fix counts of upload rows 2026-03-28 06:42:36 -07:00
c67afa7e1e
Fix link to upload detail page 2026-03-28 06:42:17 -07:00
2e0f657585
Fix upload list 2026-03-28 06:38:23 -07:00
7699e58bc3
Make upload by ID styles work correctly 2026-03-27 14:17:24 -07:00
4bbfbdb9e6
Pretty all the things I missed
My laptop didn't have lefthook running. Oops.
2026-03-27 14:06:50 -07:00
f60bde7fd9
Get rows to show on individual upload page. 2026-03-27 14:04:33 -07:00
1ad3c5a5c8
On upload redirect to upload detail page 2026-03-27 11:33:21 -07:00
747544bb58
Get file upload working
Even though the UI doesn't do anything with it yet.
2026-03-27 08:39:38 -07:00
0d1bd752a4
Fix flyover data upload link 2026-03-27 06:42:50 -07:00
88d88ec8d3
Fix link to upload pool 2026-03-27 06:40:43 -07:00
670310de15
Add upload style 2026-03-27 06:38:53 -07:00
df8cab4b07
Add style for configuration page 2026-03-27 06:36:41 -07:00
aee9bb9267
All a click in an unselected item to immediately select 2026-03-27 06:25:43 -07:00
d7c07fc65f
Move all POST endpoints to the API 2026-03-27 06:08:55 -07:00
3ff7ff05ab
Remove clear selection button 2026-03-25 21:54:06 -07:00
bf2a7582fa
Get some planning buttons wired up 2026-03-25 21:46:23 -07:00
ef412b28ec
Make upload GET an API request 2026-03-25 21:46:23 -07:00
2a92420bbe
Add flyover upload page 2026-03-24 13:50:44 -07:00
ba89b2e994
Add flyover pool upload 2026-03-24 13:48:13 -07:00
0718d88f7a
Add configuration upload page 2026-03-24 13:45:37 -07:00
a64df8a687
Fix report ID, get to where organization ID is passed through correctly 2026-03-24 11:07:48 -07:00
f33020e2b8
Add flyover card 2026-03-24 09:51:05 -07:00
0318b332bb
Fix viewing photo details 2026-03-24 09:50:15 -07:00
09ae9d0ce3
Move map interfaces to common types for sharing 2026-03-24 09:37:05 -07:00
6fe107601e
Handle address data more gracefully
Helps avoid having embarrassments at the conference
2026-03-24 09:06:42 -07:00
69eabe4e85
Use publicreport card component on planning page 2026-03-24 09:06:42 -07:00
0289bf5756
Fix retrieval of reports by ID 2026-03-24 09:06:42 -07:00
6f45325d9d
Separate publicreport display into common UI component 2026-03-24 09:06:42 -07:00
d5cf65f4cb
Add displays for public reports on the planning page 2026-03-24 09:06:42 -07:00
fb853a2bd3
Add ability to select items and display in detail view 2026-03-24 09:06:42 -07:00
e0a586b311
Clean up display of planning signal entries 2026-03-24 09:06:42 -07:00
55c8b8f1dd
Show a decent title for signals 2026-03-24 09:06:42 -07:00
8594723a0d
Clear the error message when user requests ar refresh 2026-03-24 09:06:42 -07:00
761af13270
coalesce to a valid country value 2026-03-24 09:06:42 -07:00
7f756ce8ca
Make refresh button on planning work
And experiment with separating the list entries into a separate
component
2026-03-24 09:06:42 -07:00
21a8f3029e
Remove on() from proxied arcgis map, catch errors 2026-03-24 09:06:42 -07:00
4f92fdced8
Show an error if the map fails to load 2026-03-24 09:06:42 -07:00
7360a9d2e1
Don't crash if the signal hos no address 2026-03-24 09:06:42 -07:00
b081dcf6d5
Check auth off of our API client 2026-03-24 09:06:42 -07:00
2856587aca
Remove warnings about importing defines that are macros 2026-03-24 09:06:42 -07:00
ea318af65f
Start work on the signin page 2026-03-24 09:06:42 -07:00
da62cc8f98
Add axios
We'll use it to make TypeScript-enabled API requests
2026-03-24 09:06:42 -07:00
4a835d3f16
ignore color function errors
They're from Bootstrap, they don't really matter
2026-03-24 09:06:42 -07:00
84102dd50e
Disable scss deprecation warnings 2026-03-24 09:06:42 -07:00
32f00afa8a
Fix creation of new user organizations 2026-03-24 09:06:42 -07:00
3ae88f984a
Add a .env file
This allows customizations on different machines, like my laptop vs my
desktop.
2026-03-24 09:06:42 -07:00
96237c7599
Allow for disabling OpenAI integration
For offline development
2026-03-24 09:06:42 -07:00
f90faa4732
Set the database when importing districts 2026-03-24 09:06:42 -07:00
a7fe9ee6d9
Add commands for creating the tegola user 2026-03-24 09:06:42 -07:00
9eb7022336
Provide the raw address value for public reports 2026-03-24 05:53:05 +00:00
0a79c5d945
Get dev system running on different ports
So we go Caddy to Vita to nidus-sync
2026-03-23 00:34:21 +00:00
b384252c7c
Add placeholder gen file
Without it we can't compile go.
2026-03-22 22:38:47 +00:00
47f900ab76
Switch from esbuild to vite
It just works better for debugging with VueJS
2026-03-22 22:36:43 +00:00
50643698c2
Try harder to get source maps in Vue
It's not working.
2026-03-22 19:47:04 +00:00
8d5fb1ef0b
Get map markers working on communication page 2026-03-22 19:30:11 +00:00
354c07f2bf
Fix TypeScript errors from recent changes 2026-03-22 18:27:13 +00:00
11f56bfd1c
Allow for desecting communications 2026-03-22 18:25:02 +00:00
a4a8bcdaa9
Add ArcGIS oauth refresh page 2026-03-22 18:00:30 +00:00
35fc57e8d1
Add ArcGIS integration page 2026-03-22 17:49:59 +00:00
a410bf441c
Add integration configuration 2026-03-22 17:44:59 +00:00
71ffa13167
Add organization and pesticide configuration pages 2026-03-22 17:40:40 +00:00
bcde604690
Add pesticide configuration page 2026-03-22 17:25:11 +00:00
29d98796fb
Add common formatting functions 2026-03-22 17:21:01 +00:00
de0fbd7188
Add more configuration pages 2026-03-22 17:20:46 +00:00
da8074d1e0
Make selectedSignals a set, add some emitted actions 2026-03-22 17:19:58 +00:00
b671133f88
Move more stuff to root rendering 2026-03-22 17:19:05 +00:00
6797dfa251
Harmonize style between comms and planning panes 2026-03-22 10:14:48 +00:00
b152cf9c36
Break apart the planning columns 2026-03-22 09:58:25 +00:00
0b8bea393e
Fix updates to notification counts 2026-03-22 08:04:28 +00:00
674801c8b2
Fix subscription in the store
We are back to having instant data
2026-03-22 07:57:55 +00:00
6cd5821e5f
Fix hiding the image modal 2026-03-22 07:20:11 +00:00
d4165ec2d0
Fix display of image modal, start work on fixing marking things 2026-03-22 07:16:42 +00:00
82ecf0f5d1
Add URL for sending message to the list of URLs we give out 2026-03-22 07:06:50 +00:00
9c56f148e4
Fix a bunch of styles on communications page 2026-03-22 06:40:31 +00:00
b68332afc0
Rip apart communications page into separate columns
I broke a bunch of stuff, but it'll be worth it, promise.
2026-03-22 06:36:01 +00:00
22c2df11f8
Fix the ability to mark communications signal/noise 2026-03-22 04:53:50 +00:00
978c20d72a
Fix some of the RMO pages I broke by removing SVGs 2026-03-22 04:53:23 +00:00
bacfe7218f
Fix address display 2026-03-22 04:27:49 +00:00
c73a1123d2
Actually do more stuff when we select a communication 2026-03-22 04:26:53 +00:00
7dd61a06e2
Get to where I can select communications and see them 2026-03-22 04:08:16 +00:00
5f54cfa6ed
Get a callback to fire on click. 2026-03-22 03:56:52 +00:00
ac6cd878af
Get to where the comms page at least loads
Still got some warnings, still lots broken
2026-03-22 03:33:52 +00:00
821647cef1
Actually fetch communication from the store 2026-03-22 03:03:21 +00:00
03301518f0
TypeScript checking is clean.
Tons and tons of broken functionality. Now the crawl begins.
2026-03-22 02:55:17 +00:00
d9a98e9eb2
Begin ripping apart the communications page into components
Essential to get the logic under control
2026-03-22 02:37:10 +00:00
ef552af054
remove Alpine and start fixing type errors 2026-03-22 02:36:57 +00:00
46edbbae74
Add Communication API to user URLs
We don't want to build URLs anywhere but in the server.
2026-03-22 01:33:14 +00:00
31a9490210
Get required data for communications page from user store
Which gets it from the API of course
2026-03-22 01:23:08 +00:00
21180816be
Start providing organization info and URLs is user/self
The new frontend needs it to do its work.
2026-03-22 01:22:44 +00:00
736c71eefc
Start adding other views and our initial stores 2026-03-22 00:55:48 +00:00
c75c5446f7
Add barely-compiling views for the rest of the sidebar
No way these things actually work.
2026-03-22 00:22:16 +00:00
6422609150
Set up dashboard page through VueJS 2026-03-21 23:44:14 +00:00
bf3204992e
Enable sourcemaps for debugging 2026-03-21 23:24:06 +00:00
6d6fe9e1d6
Move Intelligence file to Vue logic 2026-03-21 22:41:47 +00:00
eaeedd5356
Use common navigation code between sidebar links 2026-03-21 22:18:01 +00:00
34d14846a1
Fix main content window to render correctly with sidebar 2026-03-21 21:59:44 +00:00
d367166e77
Add vue-router for handling routing to components 2026-03-21 21:58:02 +00:00
dba1468e4d
Improve build watch plugin
Makes it much easier to see what's going on.
2026-03-21 21:44:10 +00:00
e5af41b703
Re-create dynamic nature of the sidebar 2026-03-21 21:35:32 +00:00
48d44487da
Fill out the rest of the sidebar's icons 2026-03-21 21:31:30 +00:00
1bd0adbc50
Move SVGs into the frontend build pipeline
That way it can be used in the VueJS frontend directly
2026-03-21 21:27:50 +00:00
9b8c079d79
Start sorting out basic layout elements 2026-03-21 21:06:10 +00:00
efece7733f
Migrate root of application to use a basic Vue app
We'll build from here.
2026-03-21 20:48:21 +00:00
5779242f22
Prettier everything, remove vendored bootstrap
These are installed now via pnpm
2026-03-21 19:41:51 +00:00
004a49c4e4
Update prettier to format the new file types. 2026-03-21 19:39:30 +00:00
80f4f51b02
Add bootstrap-icons, make sidebar work with bundle logic
I'm starting to get a sense how to do all of this with these new tools.
I've semi-ported the sidebar at this point.
2026-03-21 19:34:23 +00:00
f3c818a48f
Add CSS via SCSS to the frontend build pipeline 2026-03-21 19:14:51 +00:00
1e67c0090d
Show how to add a map view through typescript 2026-03-21 18:13:40 +00:00
0126d24242
Switch to using single-file components (SFC) in Vue 2026-03-21 17:51:25 +00:00
ccdb391ccc
Get VueJS working in a sample project 2026-03-21 17:45:36 +00:00
228f4a6db9
Don't group js files with images in cache contral
That's because we want the bundle to be super-cached and immutable.
2026-03-21 15:25:18 +00:00
5d8314d13b
Ignore different temp directory 2026-03-21 15:10:28 +00:00
303b4b826b
Fix export of SSEManager for SSE connection 2026-03-21 15:08:37 +00:00
cee76ddd53
Add bootstrap to the main application bundle 2026-03-21 05:47:39 +00:00
a2c3f52ab4
Fix embedded static files on production builds 2026-03-21 05:38:42 +00:00
9cbce4ff14
Fix nix build
Apparently tabs are bad.
2026-03-21 04:44:13 +00:00
0d6a6fa797
Include bootstrap and bootstrap icons in a single style bundle 2026-03-21 03:33:11 +00:00
16499fe23e
Don't error out just because we don't have a main map. 2026-03-21 03:11:22 +00:00
31947c848a
Move static outside HTML. Start work on TypeScript bundle
It's not strictly HTML, so that's just correct.

This is just worth doing while building the new TypeScript bundle
2026-03-21 03:06:59 +00:00
976a29b7d7
Create a working sample of an AlpineJS hello world 2026-03-21 02:04:11 +00:00
701f4853b5
Create a tiny working TypeScript example page 2026-03-21 01:42:22 +00:00
9b6cacda0e
Make signals include the object they are attached to (pool, report)
This means pushing the types into the common types module, which
required a refactor of a bunch of other libraries.
2026-03-21 01:19:36 +00:00
ddc63bfa91
Show pool map in planning workbench when signal is selected 2026-03-20 22:47:03 +00:00
931ea00e22
Add entry for displaying flyover pool signal 2026-03-20 22:12:53 +00:00
2cdcbb3784
When pool are green or murky, immediately create signal from them. 2026-03-20 22:11:36 +00:00
c2c1f3377a
Remove signal_pool from tegola grant
That table is no more.
2026-03-20 21:16:42 +00:00
c4359a3c81
Fix signals getting saved with correct location 2026-03-20 20:37:16 +00:00
e86cdc6764
Fix status display for RMO 2026-03-20 20:25:58 +00:00
b034fa5cf5
Fix signal lines showing the correct type 2026-03-20 19:27:56 +00:00
8c6bb7db26
Show markers on the signal map and bound them 2026-03-20 19:20:11 +00:00
94400aa808
Remove the arcgis tile map from planning/signal selection 2026-03-20 19:11:02 +00:00
23fdfc5a98
Add comments and owner info from water reports 2026-03-20 19:07:10 +00:00
2f8f579430
Fix display of water access and breeding data 2026-03-20 18:54:37 +00:00
c392029a11
Fix alpine access errors
Turns out I misunderstood how x-data and x-if work together
2026-03-20 18:52:21 +00:00
bf5c49378b
Show reporter ownership information for water reports 2026-03-20 18:38:57 +00:00
557caef8e5
Fix not null error on nuisance reports 2026-03-20 18:30:02 +00:00
067465ab35
Expect only one format from our API
What are we, LLMs?
2026-03-20 18:29:29 +00:00
3c26ebdaf2
Shorten references to nuisance data 2026-03-20 18:13:43 +00:00
daf5aa316f
Add display for nuisance source data 2026-03-20 18:09:27 +00:00
aa94cce2ad
Fix creation of signal from a communication report
I had broken this when altering the signal model to always require a
location
2026-03-20 18:03:32 +00:00
f09533c742
Auto-reload signal data when a new signal is created 2026-03-20 18:03:05 +00:00
e88f40793f
Add duration information for nuisance reports 2026-03-20 18:01:52 +00:00
76bfc09aa5
fix display of nuisance properties 2026-03-20 17:59:40 +00:00
edfd8e285f
Add location x and y to address table
For easier reference
2026-03-20 17:59:13 +00:00
441e4d45b1
Add parcel overlay to raster tile map
Makes it easier to tell what parcel we're talking about.
2026-03-20 17:07:31 +00:00
313dacd956
Remove chatty debug logs 2026-03-20 16:38:01 +00:00
9d2b757bc7
Add pools with condition popup to review map 2026-03-20 16:37:46 +00:00
c9802b78d0
Fix double-showing of distance 2026-03-20 15:50:18 +00:00
6fcaf7fb5d
Avoid crashing when oauth is null 2026-03-20 15:47:45 +00:00
9ca8ec4ce2
Handle null image location in communication page 2026-03-20 15:45:55 +00:00
29e66327ee
Stop adding users to organizations based on Arcgis Account 2026-03-20 06:04:30 +00:00
a87904f2ff
Handle photo data including NaN for location 2026-03-20 05:48:59 +00:00
42d9d2372d
Add initial user selector for impersonation page 2026-03-20 05:20:37 +00:00
68e0da1133
Add log message when we can't marshal JSON
Been seeing this in prod
2026-03-20 05:01:52 +00:00
cb34c43ef4
Improve error messages on notify failures 2026-03-19 21:29:55 +00:00
6042e7d337
Emit events on note creation 2026-03-19 21:29:55 +00:00
31a767c944
Improve capture of shutdown error 2026-03-19 21:20:20 +00:00
87961bac58
Move audio API to its own file
More consistent organization
2026-03-19 21:07:00 +00:00
c7c1c45008
Add location to signal 2026-03-19 20:49:53 +00:00
fdab54a775
Fix saving note images and transcoding 2026-03-19 20:49:17 +00:00
ba03bf9d4f
Fix audio transcode copy-paste error 2026-03-19 20:14:23 +00:00
17fb3dcdb5
Fix saving notes from Nidus
Wow, that's a serious break.
2026-03-19 20:13:53 +00:00
f2b7d30a7f
Unselect report after it's removed from the list 2026-03-19 19:32:06 +00:00
429b724cf2
Emit a created event on signal creation 2026-03-19 19:17:00 +00:00
2c4e7c4f96
Handle nuisance reports without location data 2026-03-19 19:16:39 +00:00
7a111ab9b3
Show a notification when a report is marked as a signal 2026-03-19 19:07:48 +00:00
2f1b612e9e
Move signal creation inside platform layer
This allows us to emit events with it.
2026-03-19 19:00:44 +00:00
a5b8a333d6
Fix references to service area centroid in map creation 2026-03-19 18:08:41 +00:00
908ac4faea
Make signals, not leads, from public reports. 2026-03-19 17:41:56 +00:00
2a207fd613
Fix updating notification counts on events 2026-03-19 17:22:58 +00:00
ee61b6d24b
Move review actions into the platform, emit events on change
Still not seeing updates in the sidebar, however.
2026-03-19 16:55:49 +00:00
954a4330ee
Add notifications for review tasks 2026-03-19 16:01:44 +00:00
786a6c16a3
Fix up upload by ID
Show the street number as well as the rest of the address, emit an event
when the upload is processed, actually check if pools are existing, etc.
2026-03-19 15:31:04 +00:00
97c9269215
Update file status to committed when commit completes 2026-03-19 05:53:43 +00:00
0cc0b57e33
Fix display of 'committing' files 2026-03-19 05:46:06 +00:00
dad867a356
Fix readme example use of watchexec 2026-03-19 05:45:54 +00:00
ab5840dd54
Fix references to org ID using platform org
I broke these a while ago and didn't realize because the compiler
doesn't catch them.
2026-03-19 03:57:38 +00:00
5fd85d7052
Show log when upload file is committed 2026-03-19 03:53:15 +00:00
544f99c09b
Check error state on update sql 2026-03-19 03:52:56 +00:00
6338d9f3f3
Remove chatty debug log 2026-03-19 03:52:40 +00:00
45643e8369
fix redundant log message 2026-03-19 03:52:28 +00:00
c872cebb8f
Make pool condition colors more distinctive 2026-03-19 03:42:30 +00:00
5fa4dd2884
Fix error about redundant service area 2026-03-19 03:42:14 +00:00
c039c70e3e
Switch file upload page to not use map-libre-test
That libre test was something I built when doing the changeover to
stadia maps. It's now pretty well baked, so it's better to just use it.
2026-03-19 03:30:01 +00:00
434746aa99
Allow the catch-all district to do uploads 2026-03-19 03:25:36 +00:00
2f61b224de
Prevent creating CSV uploads without a service area 2026-03-19 03:19:58 +00:00
f2ea1367e2
Allow the transaction to commit on failure in CSV processing 2026-03-19 03:19:17 +00:00
d287fa44df
Create a log for impersonation activities 2026-03-19 03:19:03 +00:00
b2eb98a66c
Fix upload list page 2026-03-19 02:51:09 +00:00
732b123342
Auto-generate report IDs, join on public_id. 2026-03-18 20:36:58 +00:00
f66d40f28b
fix bad select in migration 112 2026-03-18 20:23:57 +00:00
15766d0f86
Fix build for staging 2026-03-18 19:59:42 +00:00
bf99e0ce2e
Fix display of types on comms page 2026-03-18 19:26:49 +00:00
d03ae73285
Fix setting address on marker drag of standing water page 2026-03-18 19:26:32 +00:00
c39837faba
Make 'minutes' a bit shorter.
To avoid some line wrapping
2026-03-18 19:26:06 +00:00
28714b06b8
Fix tracking report type through the system 2026-03-18 19:25:52 +00:00
5cfd16e6a0
Fix references to report organization ID 2026-03-18 19:00:40 +00:00
685b7456b6
Return logs on comms public reports
...and start to display them. A bit.
2026-03-18 18:56:51 +00:00
21e8b9880d
Remove report_location view, add lat lng to report table 2026-03-18 18:55:50 +00:00
341c3ef6b9
Fix publicreport creation
The consistency is good, but I added some errors, like not using an enum
2026-03-18 18:45:18 +00:00
1d2570c912
Add DB model for publicreport logs
It's just way easier to track that deriving the data every time an API
request is made.
2026-03-18 17:32:06 +00:00
1e071d5ce5
Overhaul publicreport storage layer, create unified tables
This is a huge change. I was getting really sick of the split between
nuisance/water tables when more than half of the data they store is
common. I finally bit off the big work of switching it all.

This creates a single unified table, publicreport.report and copies the
existing report data into it. It also ports existing data from the
original tables into the new table.

Along with all of this I also overhauled the system for handling
asynchronous work to use a LISTEN/NOTIFY connection from the database
and a single cache table to avoid ever losing work.
2026-03-18 15:36:20 +00:00
2538638c9d
Create generic backend process, fix background interdependencies
This refactor was born out of the inter-dependency cycles developing
between the "background" module and just about every other module which
was caused by the background module becoming a dependency of every
module that needed to background work and the fact that the background
module was also supposedly responsible for the logic for processing
those tasks.

Instead the "background" module is now very, very shallow and relies
entirely on the Postgres NOTIFY logic for triggering jobs. There's a new
table, `job` which holds just a type and single row ID.

All told, this means that jobs can be added to the queue as part of the
API-level or platform-level transaction, ensuring atomicity, and
processing coordination is handled by the platform module, which can
depend on anything.
2026-03-16 19:52:29 +00:00
3a28151b09
Make structured and raw geocodes work for bulk geocoding 2026-03-16 03:51:14 +00:00
7a0fe04768
Fix references to structured geocode request 2026-03-16 01:49:41 +00:00
cc95c38ab5
Initial creation of endpoint to send messages to public reporters 2026-03-16 00:20:41 +00:00
9707e8793b
Fix display of notification area 2026-03-14 20:04:10 +00:00
70d3aef8b3
Re-select selected communication on fetch
This makes it so the UI updates with any changes we pull down.
2026-03-14 20:03:46 +00:00
a8f2c87e38
Fix display of reporter name on comms page 2026-03-14 18:14:46 +00:00
148454d392
Push update to public report event when reporter is saved 2026-03-14 18:14:30 +00:00
1075e35bca
Update communications list on event 2026-03-14 18:13:51 +00:00
94b2ff2e21
Only count publicreports that haven't been reviewed 2026-03-14 16:58:25 +00:00
a20c8918f8
Update mailer query based on changes to lead/site rel 2026-03-14 16:56:04 +00:00
f8193f7354
Default map to continental US
Because 0,0 is in the middle of the ocean
2026-03-14 16:55:40 +00:00
299b72eac3
Only show communications if we have a method to do them. 2026-03-14 16:23:09 +00:00
4c71cab973
Avoid error on photo modal without photos 2026-03-14 16:17:08 +00:00
70ebfa8ee0
Avoid making the images list null if there are no images 2026-03-14 16:13:08 +00:00
66e122f7e7
Fix notification counts being off
Double-counting water
2026-03-14 16:12:52 +00:00
1a9a72adc0
Create district catch-all, make organization on public reports not null 2026-03-14 15:53:16 +00:00
5d86da626b
Fix address input when user doesn't pick a suggestion or use geolocation 2026-03-14 02:01:48 +00:00
b29d172030
Add better support for extracting address from reverse geocode results
Stadia's API sucks. They don't really tell you what their response will
be in detail, just claim they are all the same, but they're not. Not
even a little.
2026-03-14 01:51:02 +00:00
e2af49a323
Make lead creation and invalidation for public reports work
The only thing wrong at this point that I can tell is that address
aren't being correctly populated when I reverse geocode.
2026-03-14 01:14:30 +00:00
3e1b56a266
Add notification count to user, populate sidebar via alpine 2026-03-13 21:22:34 +00:00
6fb964852f
Allow sudo to send structured SSEs 2026-03-13 18:31:43 +00:00
be1e49e524
Fix rendering of dashboard page 2026-03-13 18:21:45 +00:00
4925fe4857
Close old SSE connections, push down type strings 2026-03-13 18:21:20 +00:00
e8d865d0ab
Wire up events for creating new public reports
This involved moving a lot of stuff to the platform layer since I don't
want event interfaces leaking out.

Also this includes a fix to the user authentication which I had
previously broken by making a platform-layer user object independent of
the database layer.
2026-03-13 17:56:24 +00:00
9a5cc4cf97
Fix service area on communication page 2026-03-13 00:03:36 +00:00
f29047f723
Initiate events connection on all authenticated pages 2026-03-13 00:03:23 +00:00
44c4f17f32
Massive rework of platform layer user/organization
The goal of this rework is to make it so I can pass around platform.User
instead of a pair of models.Organization and models.User. This is useful
for reason I kind of forget now, but it started with working on
notifications and ballooned massively from there into refactoring a
number of things that were bugging me.

This also includes a tiny amount of work on server-side events (SSE).

 * background stuff lives inside the platform now, which I need for
   having it push updates through SSE
 * userfile now lives in the platform, under file, so other platform
   functions can safely use it
 * oauth is broken into pieces and inside platform because other stuff
   was calling it already, but badly.
 * notifications go into the platform as well
2026-03-12 23:49:16 +00:00
32dcc50c94
Add new view for report counts and invalidated status
Also drop site.version from the primary key.
2026-03-12 15:27:36 +00:00
9525363bc8
Disallow SVG upload for photo-upload 2026-03-12 01:23:56 +00:00
82f67bdb6c
Add basic data table and map for looking at sites 2026-03-12 01:16:41 +00:00
26bf8ceab9
Fetch more tasks after clearing one
That way we can keep a list of 20 if we have thousands.
2026-03-12 00:37:57 +00:00
20c0b4487c
Zoom in closer by default on task click 2026-03-12 00:37:43 +00:00
6d1003dcbd
Show the actual total number of tasks pending 2026-03-12 00:30:19 +00:00
40e7c8fdbe
Move discard entry away from complete button
To avoid mis-clicks
2026-03-12 00:00:40 +00:00
20025333fa
Make it possible to save the pool condition 2026-03-11 23:59:50 +00:00
f1c21a6fba
Update sites list to show proper nested authenticated layout. 2026-03-11 23:53:28 +00:00
6e9554d62d
Fix commit action on pool review page to send changes 2026-03-11 23:53:08 +00:00
d0a920b8d9
Log errors on POST, send JSON bodies back 2026-03-11 23:52:44 +00:00
5b75ac1d1c
Add initial sketch of site review page 2026-03-11 23:39:25 +00:00
62fd857b83
Enable verbose logging by default 2026-03-11 23:20:58 +00:00
a049cef651
Remap to noun-adjective on report-table/table-report 2026-03-11 23:20:38 +00:00
7231bf0aad
Add link card for sites 2026-03-11 23:13:33 +00:00
e09c412139
Make it possible to handle null reporter_contact_consent 2026-03-11 23:00:06 +00:00
e63646c9a1
Handle EOF markers when reading EXIF data 2026-03-11 22:54:41 +00:00
182fd7d229
Fix quoting in insertFlyover
I didn't know I could do this when I wrote it.
2026-03-11 22:54:22 +00:00
ce3650bc21
Set up initial implementation of completing review tasks
Insufficiently tested at this point.
2026-03-11 22:51:02 +00:00
10b4bf929f
Add resolution for tracking review_task discards 2026-03-11 22:49:55 +00:00
3ccc05d4c5
Save tiles to the database to make empty tile load faster 2026-03-11 17:01:47 +00:00
a1e6f930cb
Add lat/long to the list of reviewed items 2026-03-11 15:55:43 +00:00
edcb48f84f
Fix RMO status page to use mapbox, make selection of address fly there 2026-03-11 15:50:12 +00:00
f2eda02f26
Make RMO status page render again 2026-03-11 15:23:01 +00:00
289fac1e7e
Set marker on click on aerial imagery 2026-03-11 15:12:59 +00:00
5e7c547670
Fix map display on RMO status-by-id page
This involves rebuilding the "publicreport.report_location" view after
making a bunch of changes to making publicreport.water the consistent
name (over pool) throughout RMA tables.
2026-03-11 14:59:04 +00:00
a7c34ca3b2
Update vendor hash for new arcgis-go 2026-03-11 14:39:50 +00:00
b90b817e4c
mod tidy 2026-03-11 14:35:38 +00:00
f43fcde742
Bump to latest arcgis-go
To get access to the ErrorWithStatus return on Tile
2026-03-11 14:30:01 +00:00
3743d63692
Add proxy for managing tiles 2026-03-11 14:28:59 +00:00
d6407933f8
Avoid jumping on null map 2026-03-10 20:14:19 +00:00
c2429654c6
Add flyover data view 2026-03-10 18:05:07 +00:00
666552d0fb
Add working map with markers for pool location 2026-03-10 17:46:58 +00:00
7d791000d9
Add actual map instead of a placeholder 2026-03-10 17:02:10 +00:00
e1222778fe
Show pool condition and pre-populate the pool condition 2026-03-10 16:53:13 +00:00
a9c0c56e7c
Show pool condition and address on review page 2026-03-10 16:35:53 +00:00
fb8ee96b21
Don't populate communications with fake data 2026-03-10 16:20:28 +00:00
d2620cdf1d
Add migration to remove extra quotes from EXIF data 2026-03-10 16:18:27 +00:00
6db5bb1cbf
Don't show location data when it's empty 2026-03-10 15:50:39 +00:00
e5211d1409
Make filter buttons on comms page work 2026-03-10 15:47:30 +00:00
4ac7e29909
Sort messages by creation time 2026-03-10 15:46:17 +00:00
78d47c4035
Move address suggestion over to stadia map 2026-03-10 15:31:02 +00:00
a2b8e8f7c7
Make display of distances avoid long decimals 2026-03-10 04:58:43 +00:00
8bbe3d3cb3
Add simple history log to communications 2026-03-10 04:58:24 +00:00
cd763efb15
Require passing actual markers into SetMarkers
And frame locations of images and the report when a report is selected
2026-03-10 04:20:55 +00:00
741b60485c
Add some files I missed in earlier commits 2026-03-09 23:26:44 +00:00
355a52d100
Stop manually truncating
Browsers are better at this
2026-03-09 23:26:24 +00:00
873d5da2cd
Show reasonable address when we have none 2026-03-09 23:02:11 +00:00
ce6c6c1cc1
Initial render of standing water reports from the public 2026-03-09 22:59:21 +00:00
cd47aaba94
Allow clicking on the entire care for seleting sources 2026-03-09 22:22:04 +00:00
52f2a75ec5
Pass-through error on address creation failure 2026-03-09 22:18:39 +00:00
f6ce5f91a2
Match up distance from reporter in UI and API 2026-03-09 22:18:24 +00:00
642309520f
Handle not having a reporter name 2026-03-09 22:17:56 +00:00
2071ae9e54
Strip quotes off EXIF values before they go into the database 2026-03-09 22:10:40 +00:00
93d767c9d6
Reference new distance from report for photos 2026-03-09 22:10:15 +00:00
4cd0e05996
Add distance from report to image data 2026-03-09 22:09:35 +00:00
d3f554db92
Add photo information showing the EXIF data 2026-03-09 19:32:19 +00:00
808d6fb273
Add missing generated SQL file 2026-03-09 19:30:35 +00:00
7e60713649
re-link photo content with new payload schema 2026-03-09 19:29:48 +00:00
1be8c24235
Serialize EXIF data for images 2026-03-09 19:24:02 +00:00
7c98a52133
Provide time-of-day as fully expanded in JSON 2026-03-09 19:23:34 +00:00
55e62f3831
Show source description if provided 2026-03-09 19:23:16 +00:00
3185a17b25
Put contact icons on the same line
Saves vertical space
2026-03-09 19:12:07 +00:00
d1f542efe9
Fix image entities getting passed to frontend 2026-03-09 19:09:59 +00:00
d2eecdef90
Create address records from nuisance report data if provided 2026-03-09 19:09:42 +00:00
5af93abbb9
Disable sentry debug for now
It's littering my log output
2026-03-09 19:09:07 +00:00
034115a515
Remove lingering mapbox token reference
I'll have to fix the underlying map
2026-03-09 19:08:44 +00:00
85a3f5e578
Update script for deleting all reports 2026-03-09 18:29:33 +00:00
66beb30d93
Properly encode lat/lng on location 2026-03-09 18:29:03 +00:00
6b4c6ab942
Properly reference more address properties 2026-03-09 18:28:50 +00:00
f683cbe2a2
Remove unused function for handling removed widgets 2026-03-09 18:28:29 +00:00
3c43f72028
Fix reference to full formatted address 2026-03-09 18:28:03 +00:00
99dc9a08c0
Add 'centroid' accuracy from the database
It's used by stadiamaps
2026-03-09 18:27:47 +00:00
ac98586425
Move mosquito icon to tiny mosquito 2026-03-09 18:04:28 +00:00
15af787950
Update to using stadiamaps API 2026-03-09 18:04:07 +00:00
e932c2c473
Rework publicreport addressing
This adds the ability to link a proper address in the database to the
report and harmonizes the field names with the address table. It also
migrates away from mapbox entirely.

And I fixed the "pool" naming for the publicreports, which are supposed
to be the more generic 'water'.
2026-03-09 18:02:22 +00:00
884634a2d7
Split apart comms logic into platform-lever funcs 2026-03-09 15:03:01 +00:00
2f7ecdfae8
Move publicreport.image to have a point location 2026-03-09 00:48:44 +00:00
cfe399e44f
Add address number to public reports 2026-03-08 03:16:58 +00:00
1e80c62701
Migrate public report classes to point location
...and drop quick report tables
2026-03-08 02:43:00 +00:00
4972dd05ee
Fix rendering of signup page 2026-03-07 02:31:23 +00:00
beb4b914a0
Add additional source information and property areas 2026-03-07 02:18:40 +00:00
1970ccb13e
Show images from public reports 2026-03-07 02:02:10 +00:00
361933994f
re wire-up images 2026-03-07 01:57:18 +00:00
9b18209d24
Zoom map on comms selection 2026-03-07 01:52:59 +00:00
2f140c168e
Add ability to handle map load late 2026-03-07 01:45:53 +00:00
2c0d545fe7
Move map outside of "select report" conditional template
So we can load it at any time, and not have to reload it.
2026-03-07 01:38:05 +00:00
f1f9c8f902
Add type to communications response 2026-03-07 01:37:48 +00:00
cd4b272643
Invert colors on selection for address and created info 2026-03-07 01:26:25 +00:00
6ebf755e14
Avoid switching select item style 2026-03-07 01:25:11 +00:00
e4d75613b7
Use relative time custom element 2026-03-07 01:20:47 +00:00
cb1229a8bd
fix sending too many images 2026-03-07 01:20:06 +00:00
462e12dd6e
Add location data to comms 2026-03-07 01:09:27 +00:00
636a0379aa
Early process converting communication page to use actual data. 2026-03-06 23:45:12 +00:00
13f2ade9f4
Create configuration for setting map service on organization 2026-03-06 22:45:53 +00:00
630c6b7342
Stop connecting to email websocket
It doesn't work and is generating errors on startup
2026-03-06 22:45:28 +00:00
6081781a5f
Move communication to have more width for details. 2026-03-06 22:44:11 +00:00
228379a8a6
Make arcgis configuration page render again. 2026-03-06 22:20:26 +00:00
e57d2e73a9
Update communications dashboard to have Alpine for interactivity 2026-03-06 22:08:08 +00:00
401e6fc25f
Add fake API for proxying tile requests. 2026-03-06 22:08:08 +00:00
502a4d15df
Add organization to all authorized endpoints
We use it in filtering quite a bit.
2026-03-06 22:08:08 +00:00
e1bcbf79b1
Add tile cache and relationship to organization map layer 2026-03-06 21:13:32 +00:00
e38465aaf3
Add beginnings of logic to create the review interface 2026-03-06 19:47:12 +00:00
4494bd97cf
Rename address.geom to address.location
It's a better name.
2026-03-06 19:46:41 +00:00
e44bc62325
Update delete all tool to deal with new foreign keys 2026-03-06 19:01:00 +00:00
3bc5b1c945
Add missing organization links in pool commit logic 2026-03-06 19:00:47 +00:00
03380eba45
Add new pool review page 2026-03-06 18:56:30 +00:00
527e82031e
Remove a bunch of generated bob, add feature and review tasks 2026-03-06 18:56:30 +00:00
662188485e
Show selected signals as a table with relative time data. 2026-03-06 18:56:29 +00:00
49a109ae85
Make time-relative a separable custom element I can reuse 2026-03-06 16:48:10 +00:00
0fbd1c3fca
Avoid trying to render openmap tiles
This changed because I moved to using stadiamaps and maplibre gl
2026-03-06 16:42:16 +00:00
c254bf6b48
Fix recent reports display for RMO
I broke it when I change the multipoint map
2026-03-06 14:13:02 +00:00
de63a47c64
Add query args passing to API endpoints 2026-03-06 14:12:47 +00:00
7723f03915
Remove unused search page from RMO 2026-03-06 14:12:02 +00:00
c154ceffcf
Re-disable filter boxes 2026-03-05 23:48:33 +00:00
b183308da0
Remove annoying alert on successful lead creation 2026-03-05 23:48:19 +00:00
25cd89b8a7
Only show parcels at a high level zoom so we don't flood things 2026-03-05 23:41:12 +00:00
0e4ce01776
Save sidebar state in local storage
So it stays the way I set it.
2026-03-05 23:37:51 +00:00
9d2eee882a
Add parcel overlay to map on planning bench 2026-03-05 23:25:51 +00:00
a4a6ab8f3a
Properly bound aggregate map without animation 2026-03-05 22:35:34 +00:00
fd93d3740b
Add some data for parcel import 2026-03-05 19:40:34 +00:00
7e390bd31d
Add missing config to dashboard content
That's what caused us to include flogo.
2026-03-05 19:40:09 +00:00
e33bce7436
Add debugging around staging environment not acting like prod 2026-03-05 19:17:16 +00:00
2283aba713
Properly set bounds on multipoint map startup 2026-03-05 19:09:45 +00:00
6c922ec9df
Fix intelligence being planning-redundant. 2026-03-05 19:05:59 +00:00
72a4ef9fff
Actually update the pool location when we create the lead 2026-03-05 18:55:36 +00:00
4577fda4a9
Add pool location via click 2026-03-05 18:40:13 +00:00
f97e769d4b
Add specific location via geometry column on pool 2026-03-05 18:38:22 +00:00
121b880783
get map clicks and show the lat lng 2026-03-05 17:58:31 +00:00
fdae11f7cd
Add ArcGIS map tile display. 2026-03-05 17:46:13 +00:00
f3d38a6045
Add initial map for showing ArcGIS tiles 2026-03-05 17:43:40 +00:00
3a747b2b1d
Don't animate aggregate map, just jump there. 2026-03-05 17:40:28 +00:00
dd50035a47
Create leads from signal 2026-03-05 17:24:50 +00:00
7abf0f3c76
Remove lead strength, related comms, and priority context
This makes room for looking at the flyover images.
2026-03-05 16:06:59 +00:00
3501c38deb
Move the map to fit the pools on selection 2026-03-05 16:04:58 +00:00
8e64ba8032
Disable failing API request for unknown plan-followup object 2026-03-05 15:46:37 +00:00
138dbed251
Show markers on the map 2026-03-05 15:46:32 +00:00
8d400e9631
Make location names in JavaScript consistent 2026-03-05 15:42:12 +00:00
13cf7a7e2d
Add fake leads listing
Just to get JavaScript to stop complaining.
2026-03-05 15:41:56 +00:00
0aeba98fb0
Show short address on green pool signal 2026-03-05 14:49:39 +00:00
89197df6b0
Add fake API endpoint for creating leads 2026-03-05 14:18:10 +00:00
0f4ef9d2f8
Center map on district service area by default 2026-03-05 13:27:16 +00:00
b58199281b
Send the address with the signal 2026-03-05 13:26:10 +00:00
9ecff6794d
Highlight selected signals 2026-03-05 13:13:09 +00:00
478abf6d1b
Start to pull green pool signal from the API 2026-03-05 03:53:26 +00:00
1a22b9233d
Get much more data about signals to send to the planning dash 2026-03-05 03:17:45 +00:00
78a35e5d1f
Make parcels attached to addresses optional 2026-03-05 02:30:12 +00:00
5fa4be483b
Move uploads to 'committing' when we start comitting them 2026-03-05 02:24:51 +00:00
9345412f73
Make it possible to fail to find parcels for an address 2026-03-05 02:24:10 +00:00
dcafb14238
Fix reference to number column in address 2026-03-05 02:21:13 +00:00
31c6bf3a64
Make sure to commit the transaction when committing the CSV 2026-03-05 02:20:45 +00:00
eb03cb5857
Initialize stadia maps client on startup 2026-03-05 02:14:24 +00:00
61d9a21636
Move multipoint map to use maplibregl 2026-03-05 02:13:36 +00:00
b5636af514
Fix making immediate processing of uploads do the correct thing 2026-03-05 02:12:17 +00:00
c53ea02ff0
Create signal API first draft 2026-03-05 01:24:18 +00:00
60344e3c30
Relate compliance report requests through leads 2026-03-05 01:22:21 +00:00
a28377d8f8
Fix outside district count 2026-03-04 22:04:22 +00:00
9c3d2ba3df
Remove organization from fileupload.pool table, fix in district logic 2026-03-04 20:59:57 +00:00
8e00d3e04b
Fix setting the file to "parsed" when starting parse job 2026-03-04 19:27:33 +00:00
96514d61e2
Add both committing and import CSV jobs to the backlog 2026-03-04 19:02:11 +00:00
438c946bad
Start creating struct-based JSON encoding for API endpoints 2026-03-04 18:30:21 +00:00
daa8cb1748
Push geocoding down a layer
This makes it possible to always save address information from our
geocoder.
2026-03-04 18:29:52 +00:00
80e14568c6
Add custom CSS for intelligence, start disabling widgets 2026-03-04 14:58:59 +00:00
6959499d37
Add signal database schema 2026-03-04 14:58:43 +00:00
bee8097546
Stop creating oauth token debug files 2026-03-04 14:53:11 +00:00
6a5a59f8b8
Merge aerial flyover and pool CSV row datatypes
They are extremely similar, having both was just extra work.
2026-03-04 14:52:34 +00:00
3302990837
Insert enum value rather than renaming
I must have done something different on my dev machine because the state
doesn't match staging.
2026-03-04 12:12:40 +00:00
a19de21333
Fix missing enum value for pool condition 2026-03-04 04:02:55 +00:00
bded9127f8
Remove unused query-test 2026-03-04 03:47:40 +00:00
713dfd087e
Get proj build to work 2026-03-04 03:41:41 +00:00
70fb3b3711
Remove unused geocode test command 2026-03-04 03:41:11 +00:00
d0ba21ac58
Update go.mod to try harder to build on nix 2026-03-04 02:53:18 +00:00
6cadc8bdca
Remove proj
I'm not using it at this point.
2026-03-04 01:25:45 +00:00
079bc6f93c
Update vendor hash and add pkg-config for building chromedp 2026-03-04 00:41:39 +00:00
18c827ab90
Update dependencies 2026-03-04 00:31:45 +00:00
5894e4b55f
Bump to latest release of arcgis-go 2026-03-04 00:25:50 +00:00
c7dd53b6eb
Create logic and endpoint for confirming report location 2026-03-04 00:22:46 +00:00
b0fce4f363
Move mailer styles into scss 2026-03-03 23:52:17 +00:00
4756009d6b
Switch locator to osm bright style 2026-03-03 23:49:33 +00:00
9826f3fd70
Show map marker for property location 2026-03-03 23:42:24 +00:00
8c495a048e
Port RMO nuisance to libremap, pull sizing outside custom element
That way I can make the sized different
2026-03-03 20:52:02 +00:00
7cc43bc9ce
Add logo to the mailer lander 2026-03-03 20:47:26 +00:00
38906db816
Move standing woter to use libremap instead of mapbox 2026-03-03 20:27:33 +00:00
6aa7fa60b4
Add region (state) to address 2026-03-03 20:27:12 +00:00
0ff493cd53
Add remainder of the mailer mocks to the mailer flow 2026-03-03 17:52:46 +00:00
c9eee95cbb
Add URL links to mailer page 2026-03-03 17:31:41 +00:00
4be9c72060
Add initial landing for RMO mailer 2026-03-03 17:26:26 +00:00
0f6da8e25f
Move handler objects to common location to share with RMO 2026-03-03 17:08:58 +00:00
87fe5ec2e5
Fix rendering mocks 2026-03-02 23:38:05 +00:00
a0eee3a95f
Rework mailer database schema, add UUID to mailers
At this point, I sent out our first test mailers for Delta.
2026-03-02 23:27:55 +00:00
a7bb6181c7
Fix mailer URL and font layout 2026-03-02 19:32:17 +00:00
0c00171d8f
Fix mailer header flow around pool image 2026-03-02 18:57:51 +00:00
1df2aa72c0
Suppress flogo bar
It creates a third page.
2026-03-02 18:51:13 +00:00
ff2ec0ad14
Split out ability to upload flyover data from pool uploads
Tons of changes here, all in the name of quickly getting to where I can
create test compliance letters.
2026-03-02 18:49:02 +00:00
9939434cb3
Create secondary upload for pool data 2026-03-01 22:21:20 +00:00
8bfad892bc
Add debug endpoint for looking at tiles via GPS coord 2026-03-01 21:14:48 +00:00
89eda187be
Get map images working 2026-03-01 20:33:16 +00:00
6fb7fc7825
Debugging parcel image generation 2026-02-28 23:40:21 +00:00
1e8c7b3512
Ignore build command passwordgen 2026-02-28 23:39:55 +00:00
7def74d2c3
Fix parsing envelope out of the database 2026-02-28 23:27:13 +00:00
b062e0b2ca
Update module definition for new requirements 2026-02-28 23:26:53 +00:00
558f523e48
Add support for projections via proj 2026-02-28 23:26:36 +00:00
d4d9749431
Rework arcgis integration for arcgis-go changes and table changes 2026-02-28 23:26:08 +00:00
f19fb0ef2a
remove organization table's old arcgis columns 2026-02-28 23:24:19 +00:00
4f46e7e82f
Fix oauthforuser after rework of oauth token table 2026-02-28 23:24:00 +00:00
a5299f0cae
Re-nest settings sub-pages into configuration 2026-02-28 23:23:16 +00:00
b292f47d47
Clean up removed generated bob files 2026-02-28 23:21:14 +00:00
29bd1fab5c
Remove oauth refresh from dash, remove QR code (its in platform) 2026-02-28 23:18:25 +00:00
558412cfb4
Various new modules for mailer 2026-02-28 23:17:30 +00:00
985a2ab186
Add new platform functions for image tile and parcel 2026-02-28 23:16:43 +00:00
8455a67750
Add tools for injecting imported parcel and addresses 2026-02-28 22:56:00 +00:00
91fe244da8
Add data for handling parcel images 2026-02-28 22:54:39 +00:00
9613cac11a
Add all of Ben's mocks for the new root pages 2026-02-27 16:51:41 +00:00
5d8366015c
Initial import of planning workbench mock 2026-02-27 16:17:48 +00:00
c5c688850d
Update sidebar according to new master plan. 2026-02-27 16:05:57 +00:00
7b1ffbab12
Add new tables for storing parcel and address data 2026-02-26 18:18:33 +00:00
060e2915f1
Populate ArcGIS integration page refresh link 2026-02-25 17:39:16 +00:00
7080222fbc
Add page for configuring ArcGIS integration. 2026-02-25 17:36:19 +00:00
5e3a97272a
Nest setting object inside URL, use URL in sidebar 2026-02-25 17:21:37 +00:00
2838d8eee2
Update gitignore for various outputs 2026-02-25 16:21:12 +00:00
504939a438
Add RCS logos 2026-02-25 16:20:30 +00:00
0ab4e31d5e
Add admin map component file 2026-02-25 16:16:59 +00:00
0c6cd3b562
Add basic helper scripts for starting different binaries 2026-02-25 16:14:18 +00:00
de0d112630
Add tomtom routing test 2026-02-25 16:13:28 +00:00
c1825d8ae0
Add geocode test script
This was just useful in testing out the path for getting geocoding
through ArcGIS
2026-02-25 16:11:48 +00:00
2d2a8248c4
Add required config to signin context 2026-02-25 16:09:10 +00:00
2bb4a134b2
Add centroid information when geocoding
I would use the boundary rect, but I'm getting a 500-level error from
stadia maps
2026-02-25 16:08:32 +00:00
8feabbc489
Add bulk geocoding by goroutine worker
This is nearly as fast, and doesn't require the corp-only license from
stadia map which is 10x the cost.
2026-02-25 14:57:28 +00:00
ca88a0eaab
Add env var to skip TLS verify
This makes it much easier to debug with mitmproxy
2026-02-25 02:45:17 +00:00
6cccc16031
Add structured error output response 2026-02-25 02:44:35 +00:00
d375105de4
Remove chatty debug log line 2026-02-24 20:34:56 +00:00
4e8cb0568e
Translate "empty" as "dry" in pool conditions 2026-02-24 20:34:46 +00:00
efbef12080
Only show CSV as parse after geocoding. 2026-02-24 20:34:32 +00:00
9c6155367b
Improve display of pool conditions 2026-02-24 20:34:21 +00:00
1366e89cf6
Style uploads by status 2026-02-24 20:18:15 +00:00
1a98039264
Add missing upload platform source file 2026-02-24 20:08:57 +00:00
9ba01a2cbe
Fix links to creating pool URL 2026-02-24 20:08:43 +00:00
6180860ac0
Fix update of file upload statistics 2026-02-24 20:07:39 +00:00
c8f5408f27
Add link from root upload page to upload pool detail page. 2026-02-24 20:02:44 +00:00
a776c83557
Create API for discarding file uploads 2026-02-24 17:36:59 +00:00
c4e5369796
Add button to discard an upload 2026-02-24 17:22:20 +00:00
115c5dd71c
Continue to show header on data preview list 2026-02-24 17:10:54 +00:00
f140222dbc
Rename pool status 'empty' to 'dry', add more file upload statuses 2026-02-24 17:05:20 +00:00
7a84c81a70
Show upload details on upload section 2026-02-24 16:22:33 +00:00
b741c3e14d
Show pool uploads in the upload page
And remove a bunch of things, like employee information and field
notebooks
2026-02-24 16:14:35 +00:00
e93e4cc115
Add function for avoiding 'if err != nil' constructs everywhere. 2026-02-24 16:00:21 +00:00
424e53c78d
Disable non-functioning settings links 2026-02-24 15:41:22 +00:00
2c42ec3559
fix setting-integration template 2026-02-24 15:41:11 +00:00
e513107f75
Fix notification list template 2026-02-24 15:37:03 +00:00
dac52a879a
Move all sync pages to authenticatedHandler
Still need to fix many templates
2026-02-24 15:34:53 +00:00
85d2d0b95b
Handle disabled IP address from Voip.ms 2026-02-18 22:11:30 +00:00
4100263393
Fix email sending on the sudo page 2026-02-18 21:38:16 +00:00
2d61532dd1
Move to resty for email integration
It does a *much* better job at parsing errors
2026-02-18 21:31:47 +00:00
ed89a27d47
Update vendor hash for depolyment 2026-02-18 20:32:17 +00:00
ea1af2da53
Start to wire up sudo email, add email websocket 2026-02-18 17:01:02 +00:00
9cbb81f347
Wire up sudo email form to send emails
Probably.
2026-02-18 08:50:49 +00:00
67a7d20f6c
Actually send SMS on the test page 2026-02-18 08:28:19 +00:00
2626e044ca
Factor out common post parsing code for sms message 2026-02-18 08:09:53 +00:00
b0ee388986
Add simple example handler for admin functions 2026-02-18 08:02:32 +00:00
df0644f85b
Fix bug in handling errors from handler functions 2026-02-18 07:36:54 +00:00
9f20eda00d
Add link to sudo powers 2026-02-18 07:36:54 +00:00
ec8b7b7db9
Add mock of admin area 2026-02-18 07:07:15 +00:00
7ea66dc02e
Add user account roles 2026-02-18 07:03:32 +00:00
b4817546df
Show routing demonstration on radar page. 2026-02-18 04:57:12 +00:00
18c7a5f84b
Add faker routing 2026-02-17 22:50:16 +00:00
ee7dc1dd08
Add caching headers for production, fix css for RMO 2026-02-17 22:27:51 +00:00
58da733531
Save startup time to avoid double-sending static content 2026-02-17 22:05:47 +00:00
8bf0e24bcd
Default to showing all layers on status page 2026-02-17 21:40:04 +00:00
09783ee1a0
Update reference to mosquito tegola map 2026-02-17 21:39:50 +00:00
dbadb55e7c
Host our own icons, consolidate on bootstrap 1.13.1 2026-02-17 21:36:23 +00:00
50f0327f59
Don't extract location if there is none 2026-02-17 20:17:32 +00:00
34c2475515
Update deps for deployment 2026-02-17 20:04:16 +00:00
b9b8bd9943
Bump to arcgis-go v0.0.8 2026-02-17 19:55:06 +00:00
b78837cbab
Add service request detail page from service request list 2026-02-17 19:51:50 +00:00
b6264da972
Add service request list page 2026-02-17 19:06:51 +00:00
c439c7c8a8
Fold dispatch results into radar page 2026-02-17 18:56:02 +00:00
e5b6135e44
Add download and upload to the sidebar, along with mocks 2026-02-17 17:29:15 +00:00
61737d0288
Simplify tomtom API for getting routes 2026-02-17 17:05:53 +00:00
2a17c5c133
Add initial work on TomTom integration for routing 2026-02-17 16:23:56 +00:00
d536458280
Fix sidebar tooltips so they don't show when expanded 2026-02-17 14:58:38 +00:00
55553eb6a6
Merge routing mock and user management pages 2026-02-17 14:42:37 +00:00
eeb74643d0
Move admin dash out of mocks and into the sidebar 2026-02-17 14:26:19 +00:00
3d815b374e
Add admin area, flesh out tooltips 2026-02-17 14:22:19 +00:00
8150af5889
Add mock of the messages page 2026-02-17 07:05:31 +00:00
7d33354acb
Don't panic if we don't have a FieldseekerURL 2026-02-17 07:05:10 +00:00
1739bc6000
Add service area border to aggregate map 2026-02-17 06:49:41 +00:00
cd6bbc69a4
Make pool upload map show district border 2026-02-17 06:05:24 +00:00
5a7c9fd090
Move data out of import.district and in to organization
Then get the organization settings page to work again.

Tons of other stuff is broken now.
2026-02-17 05:33:12 +00:00
b786c88f52
Move user setting mocks to real settings 2026-02-16 20:25:57 +00:00
0b97c2cecc
Remove empty settings mock 2026-02-16 20:20:53 +00:00
d4ed987857
Move pesticide add out of mocks 2026-02-16 20:19:56 +00:00
38f64783ac
Move pesticide settings outside the mocks
And build a system for pulling common code out of the handlers
2026-02-16 20:17:41 +00:00
f6879ac094
Organize mocks
Make mock listing automatic and dynamic
Remove mocks that are implemented in some way
Move all remaining mocks into the mocks/ directory
2026-02-16 19:52:20 +00:00
421260a80a
Add radar mock 2026-02-16 19:14:58 +00:00
5b33b7ffcf
Switch cell detail page to libremap rendering 2026-02-16 18:49:07 +00:00
9d0a4b4b88
Move dashboard map to libregl 2026-02-16 18:34:40 +00:00
c256c96c45
Remove recommendations in pool upload
It's nice, but redundant
2026-02-16 17:59:50 +00:00
57a45d5ea2
Show pool condition as a badge
Looks nicer
2026-02-16 17:59:35 +00:00
a65f1e0776
Fix up attaching errors to rows 2026-02-16 17:59:18 +00:00
da7a687499
Default all new pools to being new 2026-02-16 16:49:24 +00:00
161bb809b9
Make tooltips show with styling 2026-02-16 16:49:09 +00:00
b8a5ada5dc
Make "show issues only" checkbox work correctly 2026-02-16 16:46:47 +00:00
ef569aef18
Save tags on pool rows, show errors in summary table 2026-02-16 16:38:04 +00:00
123a4bf590
Fix SRID of pool uploads
This makes our calculations correct when checking if an address is in
the district.
2026-02-16 15:47:39 +00:00
f859e372c6
Fill in correct data about the district 2026-02-16 15:26:41 +00:00
a1cc2dbaff
Add district setting page and display of district boundary 2026-02-16 15:03:26 +00:00
4c8da3b96a
Switch equipment to district in settings 2026-02-16 01:17:27 +00:00
0f7e01e8a2
Add map to pool import overview
For my own debugging really
2026-02-16 01:15:13 +00:00
77423a813c
Update scss tooling readme 2026-02-16 01:10:13 +00:00
9f78b7d9ba
Fix phone ensureInDB to not break transactions 2026-02-14 17:04:36 +00:00
ebc329fc5e
Actually set geom and h3cell for uploaded pools 2026-02-14 16:49:54 +00:00
0659b8993d
Bulk ignore empty CSV columns 2026-02-14 15:42:59 +00:00
66c9b40ead
Display pool condition on pool upload 2026-02-14 15:42:33 +00:00
9fdadcc296
Add a new "empty" pool condition 2026-02-14 15:42:17 +00:00
5d0d75ebb1
Add initial integration with stadia maps for bulk geocoding 2026-02-14 15:41:38 +00:00
4c856ab403
Quiet down startup logs 2026-02-14 15:40:33 +00:00
427b60132a
Add initial Stadia maps integration 2026-02-14 15:40:12 +00:00
2bc0e18b9e
Actually finish uploaded files 2026-02-14 05:40:27 +00:00
4d61783e18
Fix error message about condition 2026-02-14 05:05:56 +00:00
ff1cd00c96
Ensure phone numbers are in the DB before adding pool 2026-02-14 05:05:31 +00:00
ab9f16505e
Don't show errors when still processing the upload 2026-02-14 05:03:32 +00:00
a68cb7a986
Make 'upload edited file' button work 2026-02-14 04:38:17 +00:00
d83984f8df
Show errors from parsing the file 2026-02-14 04:36:47 +00:00
76f4613320
Keep track of rows that are outside of the district 2026-02-14 04:08:22 +00:00
8932f46900
Fix bug where I stopped initiating the background processes 2026-02-13 21:46:20 +00:00
26be460041
Convert timeSince to more generic timeRelative 2026-02-13 21:42:30 +00:00
f9d4206bab
Show tokens that have expired 2026-02-13 21:24:39 +00:00
b654198e3f
Simplify log output on startup 2026-02-13 21:15:09 +00:00
bdd862e649
Show actual information on oauth integration setting page 2026-02-13 21:14:46 +00:00
e475e43fef
Gracefully handle users without an organization 2026-02-13 19:20:41 +00:00
961ca9270e
Move setting logic to its own file
It's going to grow
2026-02-13 19:20:26 +00:00
38432a18c1
Show precise html marker on stadia test 2026-02-13 19:20:01 +00:00
62c724b5c2
Update to latest arcgis-go interfacets 2026-02-13 19:19:39 +00:00
a93099fd6e
Don't stop the server if there is a build error 2026-02-13 19:18:48 +00:00
b5395afe74
Add basic stadia maps test 2026-02-12 21:06:35 +00:00
a82231859d
Gracefully handle creating duplicate notifications 2026-02-12 21:06:08 +00:00
8e68230f4a
Handle changes to arcgis-go 2026-02-12 21:05:51 +00:00
bbbc06583a
Don't autobuild for cmd changes 2026-02-12 21:05:11 +00:00
b254472dcd
Rework all of the HTTP layer
At this point I have working login, I think, according to the MITMProxy
work I've done, but haven't had a successful request so far.
2026-02-12 17:40:42 +00:00
c136ff102b
Update to passing newly required context to arcgis-go 2026-02-10 21:30:58 +00:00
f02640be35
Remove fields from fieldseeker
This was done in ArcGIS by Delta.

We are way, WAY too coupled to a changeable data model.
2026-02-10 18:55:28 +00:00
a92cb52eb5
Gracefully handle lack of permissions on webhook and permission 2026-02-10 18:54:47 +00:00
b3ab8f6cef
Fix aggregate map not showing toolbox correctly 2026-02-10 18:08:34 +00:00
420e0246ed
Fix nidus favicon when logged in 2026-02-10 17:45:46 +00:00
346e294b21
Useless logging 2026-02-10 17:45:29 +00:00
47fb1b407f
Fix tegola URL in dashboard 2026-02-10 17:31:24 +00:00
b4053047ff
Fix coloring on dashboard icons 2026-02-10 16:27:32 +00:00
42d05fd8e4
Remove -prod arg on air config
Because I'm confident the templates are working in production now.
2026-02-10 16:27:13 +00:00
396cf5c586
Add page for showing notifications 2026-02-10 16:24:37 +00:00
c3e8bba822
Re-add notification indicator 2026-02-10 16:04:15 +00:00
522727cd8f
Update prompt document for mosquito LLM 2026-02-10 15:22:42 +00:00
ef205bd622
Populate district from image location on standing water 2026-02-10 15:22:27 +00:00
c74fff6da9
Match districts on pool reports too 2026-02-10 15:09:57 +00:00
5802fe4fd3
Show district branding on status page 2026-02-10 14:55:59 +00:00
28ade30d2b
Consolidate template loading logic between embedded and host FS
This solves a really annoying bug around my sidebar not loading
correctly in prod.
2026-02-10 14:40:50 +00:00
1b85ce0d78
Actually commit changes to notifications 2026-02-10 05:28:23 +00:00
e40fe55eaf
Modify email subscription table to drop district ID
I don't have time to work out all the behavior, this is just to get to
where I can release
2026-02-10 05:12:42 +00:00
d93cdbef41
Drop district_id from subscriptions 2026-02-10 04:59:27 +00:00
b25daf12fa
Add table for holding subscriptions from users 2026-02-10 04:31:57 +00:00
41f59f0518
Add missing email file 2026-02-10 04:11:52 +00:00
648e0ee567
Move emails to platform, make sure to create phone and email in DB 2026-02-10 04:07:59 +00:00
dd33c6ab5e
Add missing error page 2026-02-10 00:22:33 +00:00
6181e2968d
Remove geocode test
Until I can make it deployable
2026-02-09 22:44:42 +00:00
fe3ac88ec6
Fix breaking insert on nuisances subscription 2026-02-09 22:43:41 +00:00
b9cf98eee8
Consistently log internal errors 2026-02-09 22:43:32 +00:00
13c5529a41
Show an actual error page 2026-02-09 22:35:12 +00:00
84f1bee4f6
Compress down reporter column updates through utility function 2026-02-09 22:34:19 +00:00
f78515cc07
Fix error log reporting 2026-02-09 22:34:07 +00:00
b263d50b76
Remove debug log spam 2026-02-09 22:33:44 +00:00
8cd1a0ce02
Fix setting page URL 2026-02-09 21:52:11 +00:00
fe53a6ca2c
Create upload directories on startup 2026-02-09 21:51:57 +00:00
783910be50
Save EXIF data to the database again. 2026-02-09 21:41:27 +00:00
edd9bdcadc
Fix SVG loading for embedded filesystem, add RMO components
This was broken from the template refactor a couple days ago.
2026-02-09 21:40:58 +00:00
63193c5324
Add test for doing geocoding directly from esri
It works at this point.
2026-02-09 21:40:24 +00:00
443a13afcf
Add flag for testing embedded file system 2026-02-09 21:39:47 +00:00
316a94a6cf
Link up settings in sidebar to settings page 2026-02-09 19:57:12 +00:00
f00436b136
Count existing and new pool rows 2026-02-09 19:17:33 +00:00
515dbb54fa
Add basic pipe to provide status to the pool id page 2026-02-09 19:13:58 +00:00
d06b8f7949
Add mode data to pool upload rows, move to fileupload schema
This allows users to review the data before committing it to the
database
2026-02-09 19:03:27 +00:00
135ad2b73e
Do file upload, show list of uploads, do initial processing. 2026-02-09 18:25:44 +00:00
8d4195a024
Add script to keep track of all the DB commands to drop and reload 2026-02-09 14:31:10 +00:00
aee17d2c7a
Add jobs to process queue when csv is uploaded 2026-02-08 05:06:47 +00:00
f9c8f37cec
Add organization to file upload
So everyone in the org can see it.
2026-02-08 05:00:14 +00:00
e3535391dd
Add a bit of per-line processing to CSV file 2026-02-08 04:55:33 +00:00
e81161ca7f
Actually start reading CSV file 2026-02-08 04:46:54 +00:00
6b02b75a87
Add basic screen for pool upload from mock
It'll need work to get to where it's functional
2026-02-08 04:37:05 +00:00
fdd783c19c
Rework userfile yet again
I'm settling on the idea that strings should never be returned from the
userfile system. Instead, indicate which collection you want and pass
objects across.
2026-02-08 04:36:12 +00:00
c2d84b8734
Add missing migration 2026-02-08 04:02:12 +00:00
013ac85a70
Add logic to re-add background uploads that didn't complete 2026-02-08 04:01:48 +00:00
d17c8b8be7
Add worker process for CSV import jobs 2026-02-08 03:52:39 +00:00
f81f8def1c
Move data entry good mock to be part of upload flow 2026-02-08 03:47:48 +00:00
6716bc68c9
Make file uploads of CSV actually save to disk 2026-02-08 01:44:44 +00:00
0d55eb1da4
Define upload CSV schema, make POST actually go somewhere. 2026-02-08 00:59:41 +00:00
c4decb0afb
Fix pool upload page title 2026-02-08 00:59:17 +00:00
21fac37597
Rework files to use strings instead of duplicate functions
There's too many functions at this point.
2026-02-08 00:58:51 +00:00
d437c68403
Add pages for showing the CSV upload UI
And sample CSV file download
2026-02-07 20:02:39 +00:00
59076e9eb0
Add page for showing pool uploads 2026-02-07 18:26:47 +00:00
874d49c6a5
Update dashboard layout to work with sidebar 2026-02-07 18:04:24 +00:00
f9389de27b
Rework layouts to get embedded filesystem working again
I really hate go templates at this point.
2026-02-07 17:55:25 +00:00
db387ad28b
Update embedded template system
Just trying to get forward motion. Currently getting a nil Tree inside
the shared template holder.
2026-02-07 17:38:49 +00:00
75dba7193a
Finish removal of font awesome 2026-02-07 05:54:08 +00:00
0265e9d3ec
Rework template system, merge templates
The embedded portion doesn't work yet.
2026-02-07 05:53:38 +00:00
eb6e54a0f7
Format JavaScript files with prettier 2026-02-06 17:07:06 +00:00
951720def9
Allow multiple photos to be attached in sequence on photo upload 2026-02-06 17:06:36 +00:00
500659be4a
Make partial report suggestion handle hyphens 2026-02-06 16:56:26 +00:00
f6f54da305
Add maxlength to all inputs
Because the children broke things with it.
2026-02-06 16:56:09 +00:00
d464a5fbd0
Show a tooltip when disabling the lookup button on status page
And fix some auto-formatting
2026-02-06 16:55:29 +00:00
49b10a7d7e
Add prettier for formatting html files automatically
Because I was wasting time doing it myself
2026-02-06 16:10:09 +00:00
dbd84c1c09
Make clicking on titles select checkboxes in water report 2026-02-06 15:44:16 +00:00
e17dc142a5
Remove defunct selectInspectionType from water report
No longer used
2026-02-06 15:43:58 +00:00
a82509bebb
Remove all font-awesome fonts from RMO
This makes it possible to remove that dependency.
2026-02-06 15:43:50 +00:00
e001d23457
Properly convert browser accuracy units 2026-02-06 15:41:21 +00:00
d0b28d7f59
Handle when location is null in image location JSON query 2026-02-06 15:41:00 +00:00
39e0d9d9f3
Remove chatty oauth debug message about no ouath
Floods my dev setup log
2026-02-06 15:40:25 +00:00
57191fa222
Alter report submission page to request reporter name and consent
This also adds the new mechanism for handling notifications on reports
2026-02-06 15:39:49 +00:00
9328e7a2f8
Properly record and display pool reports 2026-02-05 21:43:29 +00:00
47c3f7320c
Fix warning parsing nil OrganizationID on notification page 2026-02-05 16:56:53 +00:00
5fc0f9fa3d
Update storage and display of nuisance report 2026-02-05 16:56:36 +00:00
ee7d99bc4c
Fix display of report type and status 2026-02-05 02:49:23 +00:00
fd86281a7e
Fix report location not showing up. 2026-02-05 02:46:22 +00:00
947ece00f1
Only show image count, re-add timeline 2026-02-05 02:36:24 +00:00
287133e35d
Major updates to report status page for nuisance reports 2026-02-05 02:24:37 +00:00
ca9ab6a6f2
Allow looking up the report by ID with the lookup button 2026-02-05 01:59:33 +00:00
41dcf6577a
Fix bad previous refactor 2026-02-05 01:59:23 +00:00
a5d2df9626
Avoid stack trace when there are no suggestions 2026-02-05 01:35:11 +00:00
c6fefc5cb0
Allow lowercase when searching for report IDs 2026-02-05 01:35:00 +00:00
f301feb537
Navigate to report status page on table click 2026-02-05 01:31:16 +00:00
d58a893651
Don't constantly redraw the report rows
Because it's wasteful and interrupts my click handlers.
2026-02-05 01:22:47 +00:00
3cdad6fd77
Hide broken map controls
I'm not sure why, but these don't work on the shadow DOM
2026-02-05 01:07:46 +00:00
96e14b2b5e
Update report count correctly 2026-02-05 01:05:41 +00:00
0e6e85825a
Avoid stacktrace on request failure 2026-02-05 01:02:38 +00:00
af516242d3
Make checkboxes hide map layers 2026-02-04 23:57:44 +00:00
df58424dd7
Suppress chatty frontend log 2026-02-04 18:08:12 +00:00
eddda2f005
Fix reference to poll submission completion 2026-02-04 17:31:55 +00:00
820f8237d1
Show pools and nuisances in separate layers on status search 2026-02-04 17:12:46 +00:00
63358e6848
Render nearby reports on the search map. 2026-02-04 16:26:30 +00:00
8c6299a7e7
Remove 'return home' button and additional information section 2026-02-04 16:13:21 +00:00
07e8de57de
Remove pool submit complete 2026-02-04 16:12:13 +00:00
f3221ec315
Add report ID suggestion to status page 2026-02-04 16:07:36 +00:00
7032f8e26b
Move status scss to correct directory 2026-02-03 23:13:19 +00:00
765e437d6c
Add header to status search page 2026-02-03 23:12:59 +00:00
fa0ac035ac
Add scss debug request endpoint
To help with debugging my scss.
2026-02-03 23:12:40 +00:00
26223ccc0a
Fix suggestion container offset 2026-02-03 23:11:19 +00:00
a9e333b73a
Add new custom element for handling report ID or location
Looks purty.
2026-02-03 22:11:54 +00:00
eb3d09c989
Move mock status to replace live status 2026-02-03 17:54:21 +00:00
8d96055ba1
Update water page to allow direct clicks 2026-02-03 17:25:14 +00:00
8bed4fa2fa
Pan slowly on click on the nuisance map 2026-02-03 17:22:32 +00:00
82adb92d06
Allow for direct click on the map to set location 2026-02-03 17:01:39 +00:00
cb3c4e8d2c
Save the template ID and email type in email_log 2026-02-02 23:58:51 +00:00
a5985b362a
Generate public_id after all other dataa
Because we use it to generate the ID.
2026-02-02 23:04:33 +00:00
a67279db88
Fix various breakages in email links 2026-02-02 22:57:19 +00:00
73ed495b72
Fix Hstore conversion 2026-02-02 21:59:24 +00:00
3bed78f742
Add endpoint for unsubscribing email addresses 2026-02-02 21:47:14 +00:00
b2737b4968
Add routes for confirming email address 2026-02-02 21:34:36 +00:00
a52f5da87a
Mark email addresses confirmed when they click the big button. 2026-02-02 20:02:57 +00:00
4f56915f15
Show a page for subscribing to emails. 2026-02-02 19:54:32 +00:00
9d7ca81508
Make 'view in browser' on emails work correctly 2026-02-02 19:34:37 +00:00
00a75a556e
Fix email sending for report notification confirmation
The links in the email don't work, but it's a first step
2026-02-02 17:00:48 +00:00
7ee2f72b8e
Add district list to report mosquitoes online
Makes it easier at a conference to find the district we're talking to
2026-02-02 14:23:22 +00:00
b77d9aa80a
Correctly set location data after reverse geocode
This fixes the location when a marker drag finishes
2026-02-02 07:14:03 +00:00
d3acb7d47d
Show district intro and name on root page 2026-02-02 06:59:40 +00:00
8b90b4c9ef
Fix error message for text job 2026-02-02 06:59:28 +00:00
b96ddf7162
Remove erroneous organization_id from migration 2026-02-02 02:23:55 +00:00
11011f3804
Fix minor error on water submission without photos 2026-02-01 04:01:30 +00:00
8bd83207dd
Make standing water form actually submit 2026-02-01 03:49:06 +00:00
0eb5118459
Reduce log spam on svg load
Now that it's working correctly
2026-02-01 03:48:40 +00:00
758d2dd9dc
Update standing water JavaScript to do geocoding 2026-02-01 03:34:21 +00:00
c15d1b1e22
Add rendering of district-specific standing water report 2026-02-01 03:24:50 +00:00
2066840a40
Add district-specific nuisance reporting page. 2026-02-01 03:14:36 +00:00
93079c5c8e
Make urls with district slugs 2026-02-01 03:07:46 +00:00
c435feeebc
Show district branding on root with correct slug 2026-02-01 02:57:58 +00:00
c5f6db0b73
Lookup district and show on report submission complete 2026-02-01 02:37:35 +00:00
d28e3e2ccc
Re-add missing Geom4326 column
I removed it on accident when I destroyed the entire database and forgot
it has to be created by hand.
2026-01-31 22:28:17 +00:00
4809ec101d
Include GPS accuracy from the browser 2026-01-31 22:25:11 +00:00
a93b921103
Only parse lat/lng if present 2026-01-31 22:24:54 +00:00
527f6a5628
Store images with nuisance reports 2026-01-31 22:14:46 +00:00
a369758eda
Improve banner on mobile devices 2026-01-31 21:47:46 +00:00
f20067b323
Actually update reports when a subscriber subscribes 2026-01-31 21:07:03 +00:00
1d7484ef4d
Confirm when a user confirms a phone number 2026-01-31 20:16:12 +00:00
a9b0a55f20
Create report platform layer
Rework phone subscription at the database layer so that we have a
seprate phone status and subscriptions to district communications.
2026-01-31 20:08:08 +00:00
cbcd998803
Migrate latest mocks for report submission 2026-01-31 16:44:41 +00:00
f9fb04bd60
Populate address input with user's location data by default 2026-01-31 16:31:21 +00:00
a86149b8d7
Update form data whenever the address is set in any way 2026-01-31 16:29:13 +00:00
f00b32075a
Ensure address value gets sent in form submit 2026-01-31 16:24:41 +00:00
caf76abe3b
Center RMO logo for wide screens 2026-01-31 16:23:58 +00:00
bf8c4ca6da
Prune data layer on nuisance report
After this, we can successfully POST the report.
2026-01-31 16:14:19 +00:00
1fbe41b725
Clean out a bunch of unused nuisance report fields
Feedback had us simplify the form significantly
2026-01-31 15:39:14 +00:00
612f2fba77
Fix file upload in custom photo upload control
Hat tip to https://web.dev/articles/more-capable-form-controls for the
details.
2026-01-31 02:18:22 +00:00
64cd2f49dc
Fix creating a report with a new phone number 2026-01-30 23:09:21 +00:00
ba22ed2733
Make photo selector show preview thumbnails 2026-01-30 22:44:07 +00:00
dc77d56573
Wire up button click on photo input 2026-01-30 22:39:43 +00:00
74717ef48c
Make photo uploader a custom component 2026-01-30 22:35:18 +00:00
250a3cbf58
Wire up the zoom on the map 2026-01-30 22:13:22 +00:00
f0fcfe548e
Set highlight color for time of day cards 2026-01-30 22:07:32 +00:00
0700b1c446
Wire up nuisance submit form to endpoint 2026-01-30 22:07:20 +00:00
de3a1d23b6
Update address input based on marker drag 2026-01-30 22:00:43 +00:00
96e3441556
Fix up marker drag end event for detecting when the marker moves 2026-01-30 21:38:29 +00:00
27927c506e
Get map to update after geocoding 2026-01-30 21:28:07 +00:00
bab8af4572
Get basic bones of the nuisance page copied from the mock 2026-01-30 20:41:02 +00:00
48c49fc73e
Use colored svgs for pond and status on RMO 2026-01-30 20:17:29 +00:00
7fb02f4788
Update root RMO to use banner and new colored icon 2026-01-30 20:14:10 +00:00
40028744ba
Fix svg pipeline to not need an explicit list 2026-01-30 20:11:17 +00:00
38b1cdbbad
Auto transform SVGs into template portions
This means I don't have to modify the files correctly by hand
2026-01-30 19:32:01 +00:00
bb9dd1754f
Start setting up structure for generating URLs
This is to eventually avoid adding URLs through hard-coded strings to
our templates.
2026-01-30 19:32:01 +00:00
9b1d75d47f
Rename htmlpage to html
Because it's going to get more tools.
2026-01-30 19:32:01 +00:00
2bd848fa97
Rename rmo/endpoint.go to rmo/root.go
Because it contains the root page logic.
2026-01-30 18:01:39 +00:00
3bf40572e2
Rename rmo/page.go to rmo/template.go
Because it contains stuff for dealing with templates.
2026-01-30 18:00:56 +00:00
87893363e5
Remove redundant request logging 2026-01-30 16:34:58 +00:00
bc27a19534
Switch to dart-sass
Faster, and hopefully less error-prone
2026-01-30 16:32:13 +00:00
5970e9c5a4
Add anonymity checkbox 2026-01-30 16:22:35 +00:00
a41fdac13b
Add checkbox granting backyard access 2026-01-30 16:14:52 +00:00
78ee0483d3
Add "this is my property" checkbox 2026-01-30 16:10:17 +00:00
e7230eb3c2
Remove redundant contact information section
It's handled in the next page.
2026-01-30 15:56:46 +00:00
708b5925a7
Various fixes to standing water report
* remove optional label and section
 * change wording on additional fields button
 * make toggle button disappear after clicking
 * Add prompt about selecting location by map or address
2026-01-30 15:55:23 +00:00
62ec73c673
Add primary color to header bar 2026-01-30 15:48:47 +00:00
c820ec91c6
Add district name to periodic updates subscription 2026-01-30 15:45:56 +00:00
c42beaca66
Make district logo a link to their website 2026-01-30 15:40:28 +00:00
2baad02a0c
Make "submit another report" link work, remove "back home" button 2026-01-30 15:32:50 +00:00
8dc23d2621
Hide the additional information button after use 2026-01-30 15:27:18 +00:00
9d5cc85757
Remove old RMO banner 2026-01-30 15:27:04 +00:00
1dea676ef9
Add banner to main RMO mock. 2026-01-30 15:22:45 +00:00
db9d4e05b7
Add missing dark/light variables
Fixes things like "bg-dark", "bg-light"
2026-01-30 15:13:38 +00:00
38d492e9da
Rework custom bootstrap theme to include all of bootstrap
I already had most of it anyway. This also fixes our buttons to have the
correct contrast.
2026-01-30 15:08:11 +00:00
207ad95840
Reorder submit complete mock contents
by request
2026-01-30 15:07:48 +00:00
bf62e20e46
Make it clear the button is for clicking 2026-01-30 03:34:38 +00:00
b0b886ac7c
Remove source location options, fix collapse style 2026-01-30 03:33:06 +00:00
e8f9bc666d
Change "roof & gutters" to "sprinklers & gutters" 2026-01-30 00:19:59 +00:00
dc30065520
Remove all optional labels from nuisance 2026-01-30 00:19:08 +00:00
8eae22cc7e
Remove nuisance severity input 2026-01-30 00:15:15 +00:00
2c565e9401
Remove 'optional' from nuisance mosquito activity input 2026-01-30 00:14:32 +00:00
7e69cf0e17
Add tooltip to nuisance report to use address or map 2026-01-30 00:13:39 +00:00
dd48d7ffd6
Nuisance mock: remove 'how your report helps' section 2026-01-30 00:08:50 +00:00
9bf407be6c
Switch to new favicon and custom css for RMO 2026-01-30 00:06:37 +00:00
d73dd9310c
Move logo to below introduction
based on feedback
2026-01-29 23:59:47 +00:00
e7681c7d6e
Change public-report to rmo
We're leaning into the branding and shorter directory names
2026-01-29 23:59:35 +00:00
bb692cced9
Return the MMS value as an ID for MMS messages. 2026-01-29 22:43:15 +00:00
e094ed3d0d
Move Twilio-specific parsing to twilio incoming API 2026-01-29 22:36:16 +00:00
9047fbb3ee
Reduce sentry debug messages in the console log
It's distracting and not useful.
2026-01-29 22:28:12 +00:00
ef5d8168f0
Actually handle incoming text messages from Voip.ms 2026-01-29 22:27:51 +00:00
6a434c458d
Add API handlers for Voip.ms incoming messages 2026-01-29 22:21:31 +00:00
981f444609
Add support for continuing background text jobs on subscription 2026-01-29 22:20:03 +00:00
d2d5f003d8
Get Voip.ms working again in the text system
Because we need it for the conference.
2026-01-29 21:53:49 +00:00
a900c23090
Update vendors and hash 2026-01-29 17:46:15 +00:00
9b152628fa
Add logging for call status 2026-01-29 17:39:11 +00:00
713cefc21a
Add tech support line 2026-01-29 17:30:21 +00:00
03c3467eea
Add RMO favicon 2026-01-29 16:46:37 +00:00
d9ccdd41b2
Add basic support for sentry 2026-01-29 15:48:15 +00:00
ef38aaf041
Add example tooltip 2026-01-29 15:05:06 +00:00
aa3e22d814
Make sidebar collapse centered and overlapping
Fix some styling stuff that was being done via JavaScript that can be
done via selectors.
2026-01-29 14:56:02 +00:00
4a99cb1166
Move sidebar toggle button into sidebar, make icon flip 2026-01-29 03:19:25 +00:00
17a5131ced
Add instructions for auto building scss 2026-01-29 02:57:49 +00:00
2dab6bcaf0
Build custom SCSS as part of nix package
This vendors-in the latest Bootstrap release SCSS files
from version 5.3.8.
2026-01-29 02:11:04 +00:00
20eda6a1d8
Make logo change with sidebar collapse 2026-01-28 22:33:32 +00:00
5d4a7a4155
Add customized CSS theme for bootstrap 2026-01-28 22:25:02 +00:00
082fdeebdd
Add basic layout test
This is testing a new way to do the main site layout that I think will
be a better fit for where I want the UI to go with a collapsable
interface.
2026-01-28 17:15:42 +00:00
bdb3c80ad7
Add Basic text message review mock 2026-01-28 14:58:13 +00:00
182175254e
Clean up old SMS callback endpoints 2026-01-28 14:57:50 +00:00
a42c5824af
Add district to LLM context, be more aggressive about trimming agent: 2026-01-27 23:25:51 +00:00
9914274d42
Wire in agent to the reporter texting system
Also rework the so the platform absorbs all the business logic that was
going in the wrong place.
2026-01-27 19:56:26 +00:00
a68b8781e7
Add ability to make LLM agent forget the conversation history
This is extremely useful for testing.

In order to do this I needed to actually deploy the migration to a bob
fork so I could start to add support for behaviors I really want.
Specifically the ability to search for ids in a slice.
2026-01-27 18:44:02 +00:00
b8e7b9b7fd
Working LLM responses and Twilio status tracking
The responses aren't good, but they do exist.
2026-01-27 14:29:55 +00:00
407b478637
Fold more text logic into the platform
Because it is better at managing the database, the comms/text package
will just be for integration.
2026-01-26 21:21:21 +00:00
e8e840ec44
Make username unique, make is_subscribed nullable 2026-01-26 21:11:31 +00:00
1cd4a31404
Start saving subscribed status 2026-01-26 20:51:26 +00:00
d2d95a1f6b
Update vendor hash for recent vendor changes 2026-01-26 20:51:07 +00:00
c0ecfe2e18
Add more log info on login failure 2026-01-26 20:41:21 +00:00
6070d50a58
Begin process of getting text responses from an LLM. 2026-01-26 20:30:06 +00:00
c276cbac0b
Add command to hash password directly
Useful for resetting passwords manually.
2026-01-26 18:42:30 +00:00
940f3901be
Redirect root mock to additional mocks 2026-01-26 16:11:00 +00:00
adc99e8871
Add ability to delay text message sending 2026-01-26 16:10:30 +00:00
ab105e16e8
Remove some user session logs that we don't need 2026-01-25 21:18:39 +00:00
82081b9609
Add API signin URL
That was we can have much more specific failure modes for API clients
2026-01-25 19:36:56 +00:00
c0b6398de2
Overhaul text messaging system to be like emails
It's a better system for organization and makes it so we can have better
logs about what gets sent.
2026-01-25 18:47:22 +00:00
5e9c0d9f11
Remove the address display
By request
2026-01-24 22:00:08 +00:00
1f52dda56d
Fix bug icon. 2026-01-24 21:58:22 +00:00
dd57cacd3b
Link up water report with confirmation page 2026-01-24 21:55:10 +00:00
16477a9f5a
Remove back button 2026-01-24 21:53:06 +00:00
0407f270ad
Fix page title for standing water 2026-01-24 21:52:54 +00:00
135e2ef77d
Put additional fields behind collapse button 2026-01-24 21:50:28 +00:00
d59c619729
Move additional details to below photo
as requested
2026-01-24 21:47:24 +00:00
d552a18c0b
Remove location details by request 2026-01-24 21:43:51 +00:00
d63e580cd3
Add basic water mock 2026-01-24 21:41:18 +00:00
c6fd6295a0
Add district header to seach page
satisfies a requirement.
2026-01-24 21:30:25 +00:00
65c3e8ee51
Add branded header for nuisance report 2026-01-24 21:29:24 +00:00
9aee938e30
Add status searching page 2026-01-24 21:00:30 +00:00
9e586ae6ef
Update wording for district landing page
At Ben's request
2026-01-24 20:45:51 +00:00
8f58859693
Make report complete page show after nuisance 2026-01-24 20:45:40 +00:00
3075814e81
Add initial nuisance report submit complete 2026-01-24 20:40:28 +00:00
e62b1ccfff
Add collapse button for additional information area 2026-01-24 20:36:05 +00:00
ee1ee1e901
Add map and photo upload to nuisance report page 2026-01-24 19:55:09 +00:00
03a97f30a8
Change roof to street gutters 2026-01-24 19:44:32 +00:00
46fcfa88ad
Remove inspection request, reorder location & contact 2026-01-24 19:36:21 +00:00
db75826e59
Add link to new nuisance mock 2026-01-24 19:32:19 +00:00
53397d2609
Add additional subtitle for districts 2026-01-24 19:19:52 +00:00
9ca470ffb6
Center logo, add "powered by" in footer 2026-01-24 19:18:27 +00:00
f549243c10
Render organization logos by 'slug'
This avoids leaking org IDs in the URL, and makes it possible to have a
district-specific root mock that works in both dev and prod.
2026-01-24 19:13:55 +00:00
45868e4bde
Update subtitle on RMO root page 2026-01-24 18:46:18 +00:00
35eaf781f3
Update icons to higher-quality in mocks 2026-01-24 18:45:11 +00:00
07c5b30dc4
Reword green pool to 'standing water' 2026-01-24 00:12:14 +00:00
3144717b0b
Remove 'quick report' 2026-01-24 00:12:05 +00:00
1dc340872c
Add initial mock root for public report 2026-01-24 00:07:04 +00:00
804059f87d
Attempt to check if I have an SSH identity before committing
I've lost too many commit messages at this point.
2026-01-23 20:37:55 +00:00
196792810b
Overhaul email sending system
Add logging and saving templates to the database for historical
accuracy.
2026-01-23 20:36:16 +00:00
3fed489258
Fix subscribe URL 2026-01-23 03:59:17 +00:00
5e6288ab9b
Add beginnings of work to save emails to database
Not tested yet
2026-01-23 03:32:06 +00:00
44fdaa6c2b
Add initial onboard email
...and patterns for how to do email stuff in the future.
2026-01-23 02:50:25 +00:00
aa7585563b
Fix erroneos copy-paste log message 2026-01-23 02:49:47 +00:00
f7d40c6d70
Update privace policy for Report Mosquitoes Online
Remove section on children - they may submit information, we don't
actually know the age of our users.

Add template variables, fix container layout.
2026-01-22 19:26:01 +00:00
1e51c5ce9e
Add notes on what to grant Tegola 2026-01-22 18:44:01 +00:00
be86a5d950
Actually add the privacy page, make values variables
So I can do different things at some point in dev/prod
2026-01-22 18:43:36 +00:00
aeaf45fa2b
Add privacy page for Nidus 2026-01-22 18:37:00 +00:00
5d8649ffe5
Send down h3 cell with API response
I broke this at some point while doing a refactor and didn't notice
until now.
2026-01-22 17:53:10 +00:00
1aa19ce707
More fixes to tegola URLs 2026-01-22 05:12:48 +00:00
964ef49a78
Fix tegola URL harder 2026-01-22 05:04:46 +00:00
d478f39800
fix tegola URL passthrough after my config changes 2026-01-22 04:50:37 +00:00
56e1e51279
fix up instructions on district import
After doing it in prod.
2026-01-22 04:50:20 +00:00
f38381eaf0
Fix embedded email templates 2026-01-22 04:32:21 +00:00
b7eb79d4f7
Remove old Voip.ms text integration
It's all Twilio now.
2026-01-22 03:57:50 +00:00
3573127bf1
Update vendor hash for building on Nix 2026-01-22 03:41:08 +00:00
480aaf0d0c
Update arcgis-go version 2026-01-22 03:39:31 +00:00
ad7ddf285c
go mod tidy, update arcgis-go 2026-01-22 03:29:19 +00:00
61d8d14fc2
Bunch of work around assigning reports to districts
I added some DB schema to track logos and to relate reports to
organizations. I reworked how GPS data comes from EXIF data on images
because it wasn't working for JPEGs. I might have broken PNGs in the
process. Also made the config options for domain names more
standardized.
2026-01-22 03:27:32 +00:00
486a2d98c2
Show location markers for images too 2026-01-21 21:10:21 +00:00
b94d09696e
Initial working marker display in shadow dom 2026-01-21 21:07:08 +00:00
bea7c28af2
Add image lookup on status page 2026-01-21 18:26:48 +00:00
c2c4d52026
Add tool to clean out test database on public reports 2026-01-21 17:55:04 +00:00
13cbf71c0f
WIP filling out report status page 2026-01-21 17:51:18 +00:00
aa3d7ab6b7
Clean up some aspects of WIP map-locator component
I was working on this and then left it for the day, so it was in sorry
shape.
2026-01-21 17:50:43 +00:00
a4afa057e3
Create version of timeSince that handles non-pointers
Useful for structs where we know we must have a value
2026-01-21 17:50:16 +00:00
2da6bba041
Render report results from the map 2026-01-21 15:59:16 +00:00
9d2253a4a2
Get search map overlay working again. 2026-01-21 15:15:52 +00:00
f4a88623af
Overhaul system for handling text messaging
Move away from "SMS" as the operative word - we're going RCS.
Move all comms processing to a separate goroutine
Rename the DB tables
2026-01-21 03:30:03 +00:00
842e6cff43
Move comms work to background goroutine
This is a sort of random checkpoint of work
 * add schema for tracking messages sent to DB
 * add terms of service and privacy policy for RCS compliance
 * standardize some things about background workers
 * update some missing stuff from generated DB code
2026-01-20 17:10:22 +00:00
8f44e57c72
Add basic robots.txt
So we get indexed.
2026-01-19 21:33:54 +00:00
98372d924d
Use proper config values in email templates 2026-01-19 21:24:34 +00:00
42caa77b3e
Use the same code paths to render the browser version of emails 2026-01-19 21:21:02 +00:00
1232a7c0ec
Show pretty report ID in email subject 2026-01-19 18:19:02 +00:00
4ab3c355c5
Parse email send response, log the email ID. 2026-01-19 18:10:17 +00:00
2c880568dd
Initial work on email templates
At this point I got a nice-looking formatted message in my mail client.
2026-01-19 17:58:30 +00:00
087f29d491
Add support for simple MMS
Tested and works, though it is a bit ugly.
2026-01-19 14:58:28 +00:00
4e294699d3
Initial test email works. 2026-01-18 03:00:48 +00:00
7abaebe496
Add support for sending SMS 2026-01-17 01:13:27 +00:00
8ab0b78e6e
Make sure they consent to get notifications in the UI. 2026-01-16 21:16:59 +00:00
079d20c086
Extract EXIF data from images
This required a schema change and actually dumps all existing photo data
from the public reports page. That's probably fine since it's not
deployed to any customers so all data is currently test data.
2026-01-16 20:16:58 +00:00
b95a3275ff
Set address to empty in quick report upload
Fixes a null value error
2026-01-16 14:51:50 +00:00
684c424131
Move imported districts to its own schema, add ref from organization
This will make it possible to assign reports to an organization
2026-01-16 14:43:26 +00:00
9b5140f0c2
Show full district details on location search 2026-01-15 23:19:31 +00:00
f6b5a1e580
Add API to query district by GPS location 2026-01-15 22:56:32 +00:00
0bb055b391
Allow clicks on cells without sources 2026-01-15 22:15:29 +00:00
e2549f0317
Make trap page that shows collection information 2026-01-15 22:12:52 +00:00
885b58a0ab
Add traps to cell details page 2026-01-15 21:00:42 +00:00
0bd1a10753
Fix click interface on aggregate map going to cell detail 2026-01-15 20:39:20 +00:00
948f967a16
Add aggregation map for traps
This also makes the first time I've done a Mapbox map within a web
component. It's not officially supported according to:

https://github.com/mapbox/mapbox-gl-js/issues/12796

but I found a codepen that had a working example:

https://codepen.io/keichan34/pen/ZEKqeEj?editors=1111
2026-01-15 20:25:00 +00:00
08c6a7f884
Make layer colors match card colors on dash 2026-01-15 19:35:13 +00:00
beb98813d9
Show icons on dashboard cards, swap colors
The color swap is so I can make the colors match in the map overlay.
2026-01-15 19:33:02 +00:00
f50f3892f2
Switch to trap counts instead of inspections on the dash
Because we can show those on a map.
2026-01-15 19:32:42 +00:00
06140a9062
Remove bob submodule, add arcgis.user
I had to remove the submodule because of the go bug at
https://github.com/golang/go/issues/77196
I found the bug because of a bug in bob itself
https://github.com/stephenafamo/bob/issues/610
This was because I'm trying to save data about the Arcgis user for use
in determining if I can set up hooks to avoid the polling for data
changes.
2026-01-15 19:20:39 +00:00
f94b89381f
Actually delete the organization in delete-org.sql 2026-01-15 04:11:19 +00:00
d46d988b4d
Make aggregate layers clean up, add service request aggregate 2026-01-15 04:10:54 +00:00
248cffd323
Rework arcgis oauth flow logic
This is several changes after the demo with Ben
 * When a user adds their oauth and they get an arcgis ID for an
   organization that exits they are added to that organization. The
   previous org isn't removed
 * All layer processing is done in a single large pool. This makes it
   much faster in aggregate
 * Some queries are done more directly instead of through custom sql
2026-01-15 01:05:21 +00:00
92294e5b16
Creat signout logic, make links use it. 2026-01-15 00:20:19 +00:00
35721e7fa6
Add some handy tools 2026-01-14 22:18:11 +00:00
c44fe26cdf
Remove debugging bonn layers 2026-01-14 21:52:59 +00:00
cd22818240
Actually exit early on empty aggregate
Previous attempt was subtly wrong.
2026-01-14 21:52:36 +00:00
a4c0e367a8
Make organization.name not-nullable, consolidate org in dash context 2026-01-14 21:50:47 +00:00
4f0b73c769
Add URL for Tegola to configuration
Avoid cross-environment pollution.
2026-01-14 21:39:58 +00:00
21a8f9622a
Avoid empty insert statement when there are no aggregations 2026-01-14 21:36:58 +00:00
e1684ce8f1
Remove "since last week" placeholders from dashboard
Also add the cute "...?" when syncing.
2026-01-14 20:53:46 +00:00
9667f34388
Fix various logo placeholders and header and favicon 2026-01-14 20:52:57 +00:00
8623773edc
Make oauth prompt use its own context type
And use the common utility to populate the user information
2026-01-14 20:15:48 +00:00
749f8aaec7
Initial work on creating custom map component
This isn't done, I'm just shifting gears.
2026-01-14 20:14:58 +00:00
ea48364d95
Set status when creating reports
Because we added this as a non-null required field a migration or two
ago.
2026-01-14 18:59:25 +00:00
6fd0ed8711
Move to using web components for custom components
They're modular, which is really nice.
2026-01-14 18:59:19 +00:00
b91718cd7c
Color districts by regionid
The colors jump as you zoom, but they still technically work, so I'm
committing it.
2026-01-14 15:52:14 +00:00
53f8857795
Show the district overlay map on the district page
I can event get the district name in the properties.
2026-01-14 00:40:01 +00:00
d60db93bf2
Make public report and sync share static assets
It just seems useful
2026-01-14 00:39:46 +00:00
81dabdf097
Update readme with how I created the working district table 2026-01-13 23:36:09 +00:00
cf06bb9f49
Break apart sync into parts more like public-reports
I like this layout makes it easier to track what functions do what and
keeps templates near their render functions.
2026-01-13 20:30:57 +00:00
96c144ca74
Clean up commented-out code from search page. 2026-01-13 19:53:00 +00:00
52a0031d16
Handle shifting location column for nuisance table
I shifted it so the three report tables have a common schema when
creating the report_location view.
2026-01-13 19:52:25 +00:00
00fd676adc
Add district table for california districts. 2026-01-13 19:47:19 +00:00
e6e8371742
Switch to pulling data from public reports on search 2026-01-13 18:33:12 +00:00
e18ce6a09e
Make fieldseeker tables key on globalid, version
This is because the objectid is not unique between organizations.
2026-01-13 04:16:24 +00:00
d02d34cbaa
Remove airport elements, populate report table instead 2026-01-12 18:17:31 +00:00
eb24d871d0
Bastardize mapbox example to query points
This is just to get the structure of what I need to integrate with
Mapbox to query a tile server and try to find point locations. In this
case, I'm getting the airport locations and barely showing them. This
saves me time understanding the pertinent APIs.

I need to adapt this to get our mosquito reports instead.
2026-01-09 23:52:35 +00:00
acaeb2129e
Begin sharing code with search page
This includes code for geocoding, building a map, and getting the user's
location.
2026-01-09 23:32:39 +00:00
28e4e88794
Fix sync to follow new pattern of function grouping. 2026-01-09 22:29:56 +00:00
653e3473b3
Fix fast load of static pages in development
Makes it much easier to iterate on JavaScript
2026-01-09 22:21:12 +00:00
75454834f4
Add mock of report search page 2026-01-09 21:31:45 +00:00
4303534396
Add logic for searching for a report and getting the status. 2026-01-09 21:02:30 +00:00
f6127af058
Correctly copy uploaded files 2026-01-09 20:25:13 +00:00
9e8ad7707c
Bump vendor hash 2026-01-09 20:11:57 +00:00
1508 changed files with 187198 additions and 114080 deletions

View file

@ -7,7 +7,7 @@ tmp_dir = "tmp"
bin = "./tmp/nidus-sync"
cmd = "go build -o ./tmp/nidus-sync ."
delay = 1000
exclude_dir = ["templates", "static", "tmp"]
exclude_dir = ["templates", "static", "cmd", "tmp"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
@ -25,7 +25,7 @@ tmp_dir = "tmp"
rerun = false
rerun_delay = 500
send_interrupt = true
stop_on_error = true
stop_on_error = false
[color]
app = ""

28
.gitignore vendored
View file

@ -1,2 +1,26 @@
nidus-sync
tmp/
.env
.sass-cache/
cmd/geocode-test/geocode-test
cmd/passwordgen/passwordgen
/db/jet/jet
districts/
flogo.log
lob/cmd/letter-create/letter-create
lob/cmd/letter-list/letter-list
lob/cmd/address-create/address-create
lob/cmd/address-list/address-list
/nidus-sync
/nidus-sync.log
node_modules/
postgrid/cmd/send-pdf/send-pdf
result
stadia/cmd/bulk-geocode/bulk-geocode
stadia/cmd/geocode-autocomplete/geocode-autocomplete
stadia/cmd/geocode-bygid/geocode-bygid
stadia/cmd/reverse-geocode/reverse-geocode
stadia/cmd/structured-geocode/structured-geocode
stadia/cmd/tile-raster/tile-raster
static/gen/
temp/
ts/gen
vite/*/.vite/

3
.gitmodules vendored
View file

@ -4,6 +4,3 @@
[submodule "go-geojson2h3"]
path = go-geojson2h3
url = git@github.com:Gleipnir-Technology/go-geojson2h3.git
[submodule "bob"]
path = db/bob
url = git@github.com:Gleipnir-Technology/bob.git

12
.prettierrc Normal file
View file

@ -0,0 +1,12 @@
{
"plugins": ["/nix/store/6kfm5qrd2bckffxphb5ylvbg3sz1657r-prettier-plugin-go-template-0.0.15-unstable-2023-07-26/lib/node_modules/prettier-plugin-go-template/lib/index.js"],
"useTabs": true,
"overrides": [
{
"files": ["*.html"],
"options": {
"parser": "go-template",
},
},
],
}

303
CLEANUP.md Normal file
View file

@ -0,0 +1,303 @@
# nidus-sync — Cleanup Tasks
This file lists code, files, and patterns that are remnants of older architectural approaches. These should be removed to reduce complexity, maintenance burden, and confusion.
---
## 1. Bob → Jet Migration (Incomplete)
**Status:** Bob is still the primary ORM. Jet was introduced May 2026 but only covers 3 schemas partially.
### 1a. Port remaining schemas from Bob to Jet
Jet-based queries exist for:
- `db/query/public/` — address, communication, communication_log_entry, compliance_report_request, feature, feature_pool, job, lead, signal, site
- `db/query/publicreport/` — compliance, image, image_exif, nuisance, report, report_image, report_log, water
- `db/query/arcgis/` — account, oauth, service_feature, service_map, user, user_privileges
Still using Bob directly (not yet ported to Jet queries):
- `platform/report/notification.go` (13 bob references)
- `platform/background/background.go` (8)
- `platform/arcgis.go` (8)
- `platform/text/send.go` (7)
- `platform/report/some_report.go` (6)
- `platform/site.go` (5)
- `platform/csv/flyover.go` (7)
- `platform/csv/pool.go` (5)
- `platform/csv/csv.go` (4)
- `platform/text/report.go` (4)
- `platform/text/phone_number.go` (3)
- `platform/publicreport/log.go` (3)
- `platform/mailer.go` (3)
- `platform/email/template.go` (2)
- `db/connection.go` (4 — bob.Tx types)
- `db/prepared.go` (2)
- `resource/review_task.go` (2)
- `rmo/status.go` (2)
- `rmo/report.go` (1)
- `rmo/mailer.go` (1)
- Plus many api/* files
### 1b. Remove Bob-generated models after migration
Once all queries are ported to Jet, delete the 103 `.bob.go` files in `db/models/`:
```
db/models/*.bob.go
```
### 1c. Remove Bob-specific helper files
These are Bob-specific and can be removed once Bob is fully replaced:
- `db/dberrors/` — Bob error types (still referenced)
- `db/dbinfo/` — Bob type info (still referenced)
- `db/models/bob_loaders.bob.go`
- `db/models/bob_where.bob.go`
### 1d. Remove Bob from go.mod and dependencies
After all Bob code is gone:
- Remove `github.com/Gleipnir-Technology/bob` from `go.mod`
- Run `go mod tidy`
### 1e. Remove Bob codegen scripts
- `db/bobgen.sh`
- `db/bobgen.yaml`
### 1f. Regenerate Jet output
The `db/jet/main.go` generator outputs to `db/gen/` but no output is currently checked in. Run the generator and ensure generated code is usable:
```bash
cd db/jet && go run .
```
---
## 2. Go HTML Templates → Vue SPA (Mostly Complete)
**Status:** Nearly all Go template routes are commented out in `sync/routes.go` and `rmo/routes.go`. Both hosts serve Vue SPAs via `static.SinglePageApp()`. Some Go template routes remain active.
### 2a. Remaining active Go template routes (sync)
These routes in `sync/routes.go` still render Go templates:
- `/oauth/arcgis/begin``getArcgisOauthBegin` (redirect, no template but in Go)
- `/oauth/arcgis/callback``getArcgisOauthCallback`
- `/mailer/pool/random``getMailerPoolRandom`
- `/mailer/mode-1``getMailer1` (generates PDF)
- `/mailer/mode-2``getMailer2` (generates PDF)
- `/mailer/mode-3/{code}``getMailer3` (generates PDF)
- `/mailer/mode-1/preview``getMailer1Preview`
- `/mailer/mode-2/preview``getMailer2Preview`
- `/mailer/mode-3/{code}/preview``getMailer3Preview`
- `/privacy``getPrivacy`
The mailer routes use `platform/pdf` which in turn uses headless Chrome (`chromedp`) to render HTML to PDF. This is legitimate server-side functionality, not just a template remnant. However, the PDF templates themselves may be candidates for migration to the Vue ecosystem.
### 2b. Remove all commented-out routes
Both `sync/routes.go` and `rmo/routes.go` have large blocks of commented-out route registrations. Remove these once migration is confirmed complete.
### 2c. Remove unused Go template files
Once all routes are ported or confirmed dead, remove the entire `html/template/` directory. The `html/` package (`html/embed.go`, `html/filesystem.go`, `html/func.go`, etc.) should also be removed once nothing references it.
### 2d. Reduce the html/ package surface
**Note:** The `html/` package is still actively imported by 40+ Go files. It provides:
- Template rendering (`html/embed.go`, `html/filesystem.go`) — mostly for mailer PDFs and privacy page
- `html.ContentConfig` — used extensively in sync/routes (mailer previews, admin pages)
- `html.MakeGet`, `html.MakePost` — HTTP handler wrappers (used by active `sync/` routes)
- `html.RespondError` — HTTP error responses
- Form parsing, image upload handling, URL building
**Short-term:** Remove the template rendering portion once mailer PDFs and privacy page are migrated.
**Long-term:** The full `html/` package can be removed only after all server-rendered pages are gone and handler wrappers are replaced with the `resource/` pattern.
---
## 3. esbuild (`build.js`) — Removed ✅
*(Completed 2026-05-09: `build.js` removed and `pkgs.esbuild` dropped from flake.nix devShell — Vite is the build tool)*
---
## 4. Legacy Static JavaScript Files
**Status:** `static/js/` contains 20 plain JavaScript files written as custom HTML elements and standalone scripts for the Go template era. These are referenced by old Go HTML templates but most of those templates are now unused.
### 4a. Files in static/js/
```
address-display.js
address-or-report-suggestion.js
address-suggestion.js
events.js
geocode.js
location.js
map-admin.js
map-aggregate.js
map-arcgis-tile.js
map-cell.js
map-locator.js
map-locator-ro.js
map-multipoint.js
map-proxied-arcgis-tile.js
map-routing.js
map-service-area.js
photo-upload.js
table-report.js
table-site.js
time-relative.js
user-selector.js
```
### 4b. Determine which are still used
The remaining active Go templates (mailer, oauth, privacy) may reference some of these. Check each active template for `<script src="/static/js/...">` references. Templates that are confirmed unused:
- All templates in `html/template/sync/` (dashboard, cell, communication-root, district, intelligence, layout, operations-root, planning-root, radar, review, sudo, upload-*) — these are replaced by Vue SPAs
- Most templates in `html/template/rmo/` — RMO routes are all commented out
### 4c. Migrate any still-needed functionality
The map-locator, address-suggestion, and photo-upload functionality has Vue equivalents in `ts/components/`. The remaining custom element patterns should be fully replaced by Vue components.
---
## 5. TomTom Integration — Removed ✅
*(Completed 2026-05-09: `tomtom/` directory removed — zero imports outside itself, Stadia Maps is now the geocoding/tile provider)*
---
## 6. Postgrid — Alternate Mail Provider
**Status:** `postgrid/` contains a single CLI tool (`cmd/send-pdf`) and a `postgrid` Go package reference in `main.go`. Lob is now the mail provider, with its own integration in `lob/`.
### 6a. Investigate and remove if unused
- Check if Postgrid is actually being used in production vs Lob
- If Lob is the chosen provider, remove `postgrid/` entirely
- Remove any Postgrid configuration references
---
## 7. Duplicate Architecture: `api/` vs `resource/`
**Status:** The `api/` package contains both route registration (`api/routes.go`) and handler functions (`api/signin.go`, `api/publicreport.go`, `api/compliance.go`, etc.). The `resource/` package provides typed resource handlers that expose `List`, `Get`, `Create`, etc. Some functionality exists in both layers.
### 7a. Consolidate handler functions
Functions in `api/` that directly handle business logic should be moved to `resource/`:
- `api/signin.go``postSignin`, `postSignout`, `postSignup`
- `api/compliance.go` — various compliance handlers
- `api/publicreport.go``postPublicreportInvalid`, `postPublicreportSignal`, `postPublicreportMessage`
- `api/sudo.go``postSudoEmail`, `postSudoSMS`, `postSudoSSE`
- `api/configuration.go``postConfigurationIntegrationArcgis`
- `api/review.go``postReviewPool`
- `api/twilio.go`, `api/voipms.go` — webhook handlers
- `api/audio.go`, `api/image.go` — media upload handlers
- `api/tile.go`, `api/debug.go` — utilities
### 7b. Standardize on resource pattern
Either move everything to `resource/` or keep both but clearly define responsibilities:
- `resource/` — domain resource CRUD + URI generation
- `api/` — route registration + HTTP concerns only
Currently the split is unclear and some `api/` files do substantial business logic.
---
## 8. `arcgis-go` Submodule — Not Checked Out
**Status:** The `arcgis-go` submodule (referenced in `.gitmodules`) is not checked out (empty directory). The external `github.com/Gleipnir-Technology/arcgis-go` package is used via `go.mod` instead.
### 8a. Remove submodule
```bash
git submodule deinit arcgis-go
git rm arcgis-go
```
Verify that all code references use the external package, not a local path.
---
## 9. `go-geojson2h3` Local Copy
**Status:** `go-geojson2h3/` is also a submodule. The external package `github.com/Gleipnir-Technology/go-geojson2h3/v2` is imported in `go.mod`. Only `h3utils/h3.go` references it.
### 9a. Consolidate
- If the local copy isn't needed, remove the submodule
- If local modifications exist, merge upstream or maintain intentionally with documentation
---
## 10. Old Generated Files & Artifacts
### 10a. `query.go` at project root — Removed ✅
### 10b. `db/sql/` directory
Contains `.bob.go` and `.bob.sql` files — these are Bob-style named queries. Once Bob is removed, these can be cleaned up or migrated to Jet equivalents.
### 10c. `static/gen/main.js`
A leftover built artifact. The new build output goes to `static/gen/sync/` and `static/gen/rmo/` via Vite. Ensure `static/gen/` is in `.gitignore` and the stale `main.js` is removed.
### 10d. `static/css/placeholder`
Empty placeholder file. Remove.
---
## 11. Nix devShell Cleanup
**Status:** `flake.nix` devShell includes several tools from older workflows:
### 11a. Potentially unnecessary devShell packages
- `pkgs.esbuild` — replaced by Vite (keep only if `build.js` is retained)
- `pkgs.dart-sass` — Vue/Vite uses the `sass` npm package; check if Go code invokes dart-sass directly
- `pkgs.autoprefixer` — may not be needed with Vite's built-in PostCSS
---
## 12. Start Scripts — Consolidate
**Status:** Four start scripts exist:
| Script | Purpose |
|--------|---------|
| `start-air.sh` | Development with air (live reload) |
| `start-flogo.sh` | Unknown (references `flogo`) |
| `start-nidus-sync.sh` | Production-like direct run |
| `start-nix-built.sh` | Run Nix-built output |
`start-flogo.sh` may be a remnant. Investigate and remove if unused.
---
## Priority Summary
1. **High impact, low effort:**
- ~~Remove `tomtom/` (unused, no imports)~~
- ~~Remove `build.js` (dead, replaced by Vite)~~
- Remove commented-out routes in `sync/routes.go` and `rmo/routes.go`
- ~~Remove `query.go` commented-out code~~
- Remove `static/gen/main.js` stale artifact
- Remove `static/css/placeholder`
2. **Medium impact, medium effort:**
- Remove unused Go HTML templates (confirm which are still active first)
- Remove unused `static/js/` files (verify against active templates)
- Remove `arcgis-go` submodule
- Clean up Nix devShell
3. **High impact, high effort:**
- Complete Bob → Jet migration across all schemas
- Remove Bob-generated models, helpers, scripts
- Remove Bob from go.mod
- Consolidate `api/` and `resource/` handler patterns
- Remove `html/` package (after all Go templates are gone)

207
HISTORY.md Normal file
View file

@ -0,0 +1,207 @@
# nidus-sync — Project History
## Overview
nidus-sync is a dual-tenant mosquito abatement platform serving two domains:
- **RMO** (`report.mosquitoes.online`) — Public-facing mosquito/water/nuisance reporting
- **Sync** (`sync.nidus.cloud`) — Administrative dashboard for vector control districts
The project was started in November 2025 and has undergone several major architectural shifts across ~1655 commits spanning 6 months.
---
## Timeline
### Phase 1: Foundation (November 2025)
**Nov 3 Nov 13: Project bootstrap**
- Initial Go project with Nix build system (`flake.nix`, `default.nix`)
- Basic `net/http` web serving with `gorilla/mux` routing
- Go `html/template` server-side rendering
- Bob ORM integration (`github.com/Gleipnir-Technology/bob`) for PostgreSQL — code-generated models via `bobgen`
- ArcGIS OAuth integration for user authentication
- ArcGIS Fieldseeker data synchronization (treatment areas, inspections, breeding sources, etc.)
- MapBox GL JS integration for heatmap visualization
- Dashboard with login, basic CRUD mocks
**Nov 13 Nov 24: Logging & DB restructuring**
- Migration from standard `log` to `zerolog` for structured, colorized output
- Database logic moved into a separate `db/` subdirectory
- Clean shutdown logic, token refresh loops
**Key characteristics:** Monolithic Go server, HTML templates, Bob ORM, MapBox maps, ArcGIS OAuth
---
### Phase 2: Fieldseeker & Schema Evolution (December 2025)
**Dec 2 Dec 24: Fieldseeker schema v2**
- Bob codegen updated to latest version
- Fieldseeker schema captured on OAuth connect and stored locally
- Dynamic SQL functions replacing hardcoded per-table sync logic
- Old Fieldseeker tables removed, v2 generated tables used
- Note/image audio support added
- MMS file downloads from SMS webhooks
**Key characteristics:** Bob-generated fieldseeker models, prepared SQL functions, SMS/MMS debugging
---
### Phase 3: Architecture Maturation (January 2026)
**Jan 2 Jan 8: Domain split & template system**
- WIP pass-through models concept ("Checkpoint on initial idea for passing through models")
- Massive reorganization: templates split into `rmo/` (public) and `sync/` (admin) subdirectories
- `html/` package created with embedded template loading
- Bob submodule removed, `arcgis-go` became external dependency
- Public report domain support added
- Version bumped 7 times in rapid iteration (v0.0.4 → v0.0.10)
**Jan 8 Jan 31: Platform Layer emergence**
- "Report platform layer" introduced (`a9b0a55f`) — initial abstraction between HTTP handlers and database
- Address suggestion and map-locator components via custom HTML elements
- SVG auto-transformation into Go templates
- Report submission forms wired up (nuisance, water)
- Email template system
**Key characteristics:** Two-domain architecture (RMO/Sync), `html/` template package, platform layer beginning, custom element web components
---
### Phase 4: Map Migration & Platform Expansion (February 2026)
**Feb 1 Feb 28: Map provider transition**
- MapBox → MapLibre GL (open-source fork) via `maplibre-gl`
- Stadia Maps integration for tile serving and geocoding (Feb 12-14)
- TomTom routing integration added (Feb 17)
- Bulk geocoding via Stadia
- Parcel image generation debugging
**Platform layer expansion:**
- Emails moved to platform layer
- Phone/SMS support
- OAuth integration settings
- Upload platform functions
- QR code and image tile moved into platform
- Admin map components
**Key characteristics:** MapLibre/Stadia replacing MapBox, TomTom added, platform layer expanding, heavy template iteration
---
### Phase 5: VueJS Revolution (March 2026) — 448 commits
**Mar 5 Mar 12: Pre-Vue cleanup**
- Stadia Maps client initialization
- Signal database schema added
- Review task/mailer schema rework
- Generated Bob files pruned
**Mar 12: Massive platform layer rework** (`44c4f17f`)
- User/organization handling restructured in platform layer
- Signal creation moved inside platform
**Mar 18 Mar 22: VueJS Migration** (the biggest architectural shift)
- Mar 18: Auto-generated report IDs
- Mar 21: **VueJS introduced** — begins with TypeScript bundle, then Vue SFC components, vue-router, Bootstrap/SCSS integration
- Mar 21: Dashboard, Intelligence, sidebar all moved to Vue
- Mar 22: **esbuild replaced by Vite** (`47f900ab`) — `vite/` directory with separate configs for `sync` and `rmo` SPAs
- Mar 22: TypeScript checking clean across entire frontend
- Mar 23: Public report card component, auth checks off API client
- Mar 24-31: Communication page ripped into components, impersonation support, users page
**Key characteristics:** VueJS 3 + TypeScript + Vite frontend, Pinia stores, vue-router, SCSS, SPA architecture replacing server-rendered Go templates
---
### Phase 6: Compliance & Communication (April 2026) — 454 commits
**Apr 1 Apr 9: RMO frontend & resources**
- Resource layer expanded (user, avatar, district, nuisance, water, compliance resources)
- RMO frontend checkpoint — Vue ports of public-facing pages
- TS types migrated into API module
- Old bundle paths removed, old SPA generation removed
**Apr 10 Apr 17: Compliance workflow**
- Compliance report creation, mailer flow
- Site/pool review tasks
- Stadia Maps cache, direct tile access
- OAuth refresh in frontend
- Image upload components
**Apr 17 Apr 25: Communication system**
- Background jobs reworked for shorter transactions
- Lob (physical mail) integration — direct API client, address creation, letter events
- QR code generation moved to API
- Compliance report evidence, mailer views
- Vue map system generalized (`cad01e68`)
**Apr 25 Apr 30: Map & communication polish**
- VueJS reimplementation of address/report suggestion
- Communication workbench with map, list, detail views
- Text message log, email/phone display
- Compliance card detail display
- SSE event system with status vs resource message distinction
- Systemd socket activation for downtime-free deploys
- Sentry error tracking for Vue frontend
**Key characteristics:** Compliance/mailer operational, communication system born, Lob integration, Sentry, generalized Vue map system
---
### Phase 7: Jet Migration & Cleanup (May 2026) — 46 commits so far
**May 1 May 9: SQL generation transition**
- **Jet (go-jet/jet) introduced** — type-safe SQL builder replacing Bob's query building
- Custom Jet generator created with geometry/Box2D type support (`db/jet/main.go`)
- `publicreport` schema ported to Jet
- `arcgis` schema ported to Jet (compiles, not fully tested per commit message)
- New `communication` table added
- Communication marking workflow (invalid, pending-response, possible-issue, possible-resolved)
- Linting: `golangci-lint` added to lefthook, per-file linting
- Cleanup of legacy generated columns (latitude/longitude), string-based queries
- Centralized error handler for Vue sync app
**Key characteristics:** Bob→Jet transition in progress, communication workflow, code quality improvements
---
## Architectural Patterns (by layer)
### Current architecture stack
```
┌─────────────────────────────────────────────────┐
│ Vue 3 SPA (TypeScript) │
│ ts/ — shared components, composables, stores │
│ vite/sync/ — admin SPA entry │
│ vite/rmo/ — public SPA entry │
├─────────────────────────────────────────────────┤
│ Go HTTP Server (gorilla/mux) │
│ api/routes.go — central route registration │
│ resource/ — resource handlers (REST patterns) │
│ sync/ — remaining Go template routes │
│ rmo/ — remaining Go template routes │
├─────────────────────────────────────────────────┤
│ platform/ — business logic layer │
│ (address, compliance, communication, district, │
│ email, fieldseeker, mailer, publicreport, │
│ review, signal, text, user, upload, etc.) │
├─────────────────────────────────────────────────┤
│ db/ — database access │
│ db/models/ — Bob-generated models (103 files) │
│ db/query/ — Jet-based query functions │
│ db/prepared.go — prepared SQL functions │
├─────────────────────────────────────────────────┤
│ PostgreSQL │
└─────────────────────────────────────────────────┘
```
### Pattern: Platform Layer
Introduced January 2026, the `platform/` package encapsulates business logic between HTTP handlers and the database. It grew from initial report handling to encompass users, organizations, emails, texts, compliance, communications, signals, geocoding, tiles, uploads, and more.
### Pattern: Resource Layer
Added MarchApril 2026, `resource/` provides typed REST resource handlers with URI generation (via mux route naming). Resources are instantiated with a `resource.NewRouter()` and expose methods like `List`, `Get`, `Create`, `Update`, `Delete` that return domain types. This replaced ad-hoc handler functions in `api/`.
### Pattern: Dual SPA + API
Since late March 2026, both domains serve Vue SPAs for most routes, with the Go server acting as an API backend. The `static.SinglePageApp()` handler serves the Vite-built output and falls back to `index.html` for client-side routing. Some Go template routes remain for mailer PDF generation, OAuth flows, and previews.

View file

@ -2,6 +2,25 @@
This is the software that powers [Nidus Cloud Sync](https://sync.nidus.cloud).
## Administration
### Password resets
If you need to manually reset a password you can do so with:
```
$ nix-shell -p genpass
$ genpass 12
abc123abc123
# this is from nidus, installed on deployment servers at the system layer
$ passwordgen
Please enter your password: abc123abc123
Password: abc123abc123
Hash: $2a$14$hdtoAtP7joczutY3bxaFqemBApH8xc5NbXLvDQqBfdzWV3jGSy4zi
$ psql -d nidus-sync
nidus-sync=> update user set password_hash='$2a$14$hdtoAtP7joczutY3bxaFqemBApH8xc5NbXLvDQqBfdzWV3jGSy4zi' where id=<something>;
```
## Building from source
First, you'll need [Nix](https://nix.dev).
@ -13,6 +32,15 @@ nix develop
go build .
```
## Building Custom Theme
We're using a customized Bootstrap theme for this site. You'll need to build the SCSS into CSS:
```
nix develop
sass --style=compressed --trace "$SASS_SRC_DIR/custom.scss":"$CSS_OUTPUT_DIR/bootstrap.css"
```
## Running
You'll need a number of environment variables for configuring things;
@ -31,6 +59,18 @@ You'll need a number of environment variables for configuring things;
> BASE_URL=https://sync.nidus.cloud ARCGIS_CLIENT_ID=foo ARCGIS_CLIENT_SECRET=bar POSTGRES_DSN='postgresql://?host=/var/run/postgresql&dbname=nidus-sync' ./nidus-sync
```
### Districts
There's a table containing district information in the database, `import.district`. It was created with:
```
psql
CREATE SCHEMA import;
shp2pgsql -s 3857 -c -D -I CA_districts.shp import.district | psql -d nidus-sync
psql -d nidus-sync
ALTER TABLE import.district ADD COLUMN geom_4326 geometry(MultiPolygon,4326) GENERATED ALWAYS AS (ST_Transform(geom, 4326)) STORED;
```
## Hacking
### air
@ -62,3 +102,39 @@ This uses [goose](https://github.com/pressly/goose). You can use the goose comma
> GOOSE_DRIVER=postgres GOOSE_DBSTRING="dbname=nidus-sync sslmode=disable" goose down
> GOOSE_DRIVER=postgres GOOSE_DBSTRING="dbname=nidus-sync sslmode=disable" goose up
```
### svg icons
These icons are generated as part of the build system. You can generate them manually with:
```
pnpm generate-icons
```
This will produce an scss file at `ts/gen/custom-icons.scss`
### typescript
In order to work on the TypeScript code you'll need to install the dependencies locally in your dev environment:
```
nix develop
pnpm install
```
You can then generate the TypeScript with:
```
pnpm watch
```
The only page that works right now is `https://sync.nidus.cloud/template-test`
### watchexec
For iterating on styles
```
watchexec -e scss sass scss/custom.scss:static/gen/css/bootstrap.css
```

View file

@ -2,87 +2,77 @@ package api
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"time"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/lint"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/queue"
"github.com/Gleipnir-Technology/nidus-sync/userfile"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/Gleipnir-Technology/nidus-sync/resource"
"github.com/Gleipnir-Technology/nidus-sync/version"
//"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
func apiAudioPost(w http.ResponseWriter, r *http.Request, u *models.User) {
id := chi.URLParam(r, "uuid")
noteUUID, err := uuid.Parse(id)
if err != nil {
http.Error(w, "Failed to decode the uuid", http.StatusBadRequest)
return
}
/*
type renderer struct {
}
func (ren *renderer) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
*/
// In the best case scenario, the excellent github.com/pkg/errors package
// helps reveal information on the error, setting it on Err, and in the Render()
// method, using it to set the application-specific error code in AppCode.
type ResponseErr struct {
Error error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code
var payload NoteAudioPayload
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read the payload", http.StatusBadRequest)
return
}
if err := json.Unmarshal(body, &payload); err != nil {
debugSaveRequest(body, err, "Audio note POST JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
setter := models.NoteAudioSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(u.ID),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Duration: omit.From(payload.Duration),
Transcription: omitnull.FromPtr(payload.Transcription),
TranscriptionUserEdited: omit.From(payload.TranscriptionUserEdited),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
if err := db.NoteAudioCreate(context.Background(), u.R.Organization, u.ID, setter); err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
StatusText string `json:"status"` // user-level status message
AppCode int64 `json:"code,omitempty"` // application-specific error code
ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
}
func apiAudioContentPost(w http.ResponseWriter, r *http.Request, u *models.User) {
u_str := chi.URLParam(r, "uuid")
audioUUID, err := uuid.Parse(u_str)
if err != nil {
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
return
}
err = userfile.AudioFileContentWrite(audioUUID, r.Body)
if err != nil {
log.Printf("Failed to write content file: %v", err)
http.Error(w, "failed to write content file", http.StatusInternalServerError)
}
queue.EnqueueAudioJob(queue.AudioJob{AudioUUID: audioUUID})
w.WriteHeader(http.StatusOK)
func (e *ResponseErr) Render(w http.ResponseWriter, r *http.Request) error {
http.Error(w, e.StatusText, e.HTTPStatusCode)
return nil
}
func handleClientIos(w http.ResponseWriter, r *http.Request, u *models.User) {
func errRender(err error) *ResponseErr {
log.Error().Err(err).Msg("Rendering error")
return &ResponseErr{
Error: err,
HTTPStatusCode: 500,
StatusText: "Error rendering response",
ErrorText: err.Error(),
}
}
type Renderable interface {
Render(http.ResponseWriter, *http.Request) error
}
func renderShim(w http.ResponseWriter, r *http.Request, renderer Renderable) error {
return renderer.Render(w, r)
}
func renderList(w http.ResponseWriter, r *http.Request, data []Renderable) error {
return nil
}
func handleClientIos(w http.ResponseWriter, r *http.Request, u platform.User) {
var sinceStr string
err := r.ParseForm()
if err != nil {
render.Render(w, r, errRender(fmt.Errorf("Failed to parse GET form: %w", err)))
err = renderShim(w, r, errRender(fmt.Errorf("Failed to parse GET form: %w", err)))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
} else {
sinceStr = r.FormValue("since")
@ -94,14 +84,20 @@ func handleClientIos(w http.ResponseWriter, r *http.Request, u *models.User) {
} else {
since, err = parseTime(sinceStr)
if err != nil {
render.Render(w, r, errRender(fmt.Errorf("Failed to parse 'since' value: %w", err)))
err = renderShim(w, r, errRender(fmt.Errorf("Failed to parse 'since' value: %w", err)))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
}
csync, err := platform.ContentClientIos(r.Context(), u, since)
if err != nil {
render.Render(w, r, errRender(err))
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
@ -115,68 +111,22 @@ func handleClientIos(w http.ResponseWriter, r *http.Request, u *models.User) {
Fieldseeker: toResponseFieldseeker(csync.Fieldseeker),
Since: since_used,
}
if err := render.Render(w, r, response); err != nil {
render.Render(w, r, errRender(err))
if err := renderShim(w, r, response); err != nil {
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
}
func apiImagePost(w http.ResponseWriter, r *http.Request, u *models.User) {
id := chi.URLParam(r, "uuid")
noteUUID, err := uuid.Parse(id)
if err != nil {
http.Error(w, "Failed to decode the uuid", http.StatusBadRequest)
return
}
var payload NoteImagePayload
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read the payload", http.StatusBadRequest)
return
}
if err := json.Unmarshal(body, &payload); err != nil {
debugSaveRequest(body, err, "Image note POST JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
setter := models.NoteImageSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(u.ID),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
err = db.NoteImageCreate(context.Background(), u.R.Organization, u.ID, setter)
if err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiImageContentPost(w http.ResponseWriter, r *http.Request, u *models.User) {
u_str := chi.URLParam(r, "uuid")
imageUUID, err := uuid.Parse(u_str)
if err != nil {
log.Error().Err(err).Msg("Failed to parse image UUID")
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
}
err = userfile.ImageFileContentWrite(imageUUID, r.Body)
if err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusOK)
log.Printf("Saved image file %s\n", imageUUID)
fmt.Fprintf(w, "PNG uploaded successfully")
}
func apiMosquitoSource(w http.ResponseWriter, r *http.Request, u *models.User) {
func apiMosquitoSource(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
@ -185,23 +135,32 @@ func apiMosquitoSource(w http.ResponseWriter, r *http.Request, u *models.User) {
query.Limit = 100
sources, err := platform.MosquitoSourceQuery()
if err != nil {
render.Render(w, r, errRender(err))
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
data := []render.Renderer{}
data := []Renderable{}
for _, s := range sources {
data = append(data, NewResponseMosquitoSource(s))
}
if err := render.RenderList(w, r, data); err != nil {
render.Render(w, r, errRender(err))
if err := renderList(w, r, data); err != nil {
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
}
}
func apiTrapData(w http.ResponseWriter, r *http.Request, u *models.User) {
func apiTrapData(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
@ -210,23 +169,32 @@ func apiTrapData(w http.ResponseWriter, r *http.Request, u *models.User) {
query.Limit = 100
trap_data, err := platform.TrapDataQuery()
if err != nil {
render.Render(w, r, errRender(err))
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
data := []render.Renderer{}
data := []Renderable{}
for _, td := range trap_data {
data = append(data, NewResponseTrapDatum(td))
}
if err := render.RenderList(w, r, data); err != nil {
render.Render(w, r, errRender(err))
if err := renderList(w, r, data); err != nil {
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
}
}
func apiServiceRequest(w http.ResponseWriter, r *http.Request, u *models.User) {
func apiServiceRequest(w http.ResponseWriter, r *http.Request, u platform.User) {
bounds, err := parseBounds(r)
if err != nil {
render.Render(w, r, errRender(err))
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
query := db.NewGeoQuery()
@ -234,16 +202,22 @@ func apiServiceRequest(w http.ResponseWriter, r *http.Request, u *models.User) {
query.Limit = 100
requests, err := platform.ServiceRequestQuery()
if err != nil {
render.Render(w, r, errRender(err))
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
return
}
data := []render.Renderer{}
data := []Renderable{}
for _, sr := range requests {
data = append(data, NewResponseServiceRequest(sr))
data = append(data, types.ServiceRequestFromModel(sr))
}
if err := render.RenderList(w, r, data); err != nil {
render.Render(w, r, errRender(err))
if err := renderList(w, r, data); err != nil {
err = renderShim(w, r, errRender(err))
if err != nil {
http.Error(w, fmt.Sprintf("render shim: %v", err), http.StatusInternalServerError)
}
}
}
@ -284,16 +258,6 @@ func parseBounds(r *http.Request) (*db.GeoBounds, error) {
return &bounds, nil
}
func errRender(err error) render.Renderer {
log.Error().Err(err).Msg("Rendering error")
return &ResponseErr{
Error: err,
HTTPStatusCode: 500,
StatusText: "Error rendering response",
ErrorText: err.Error(),
}
}
func webhookFieldseeker(w http.ResponseWriter, r *http.Request) {
// Create or open the log file
file, err := os.OpenFile("webhook/request.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
@ -302,17 +266,32 @@ func webhookFieldseeker(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer file.Close()
defer lint.LogOnErr(file.Close, "close request log")
// Write timestamp
timestamp := time.Now().Format("2006-01-02 15:04:05")
fmt.Fprintf(file, "\n=== Request logged at %s ===\n", timestamp)
_, err = fmt.Fprintf(file, "\n=== Request logged at %s ===\n", timestamp)
if err != nil {
log.Error().Err(err).Msg("writing response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Write request line
fmt.Fprintf(file, "%s %s %s\n", r.Method, r.RequestURI, r.Proto)
_, err = fmt.Fprintf(file, "%s %s %s\n", r.Method, r.RequestURI, r.Proto)
if err != nil {
log.Error().Err(err).Msg("writing response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Write all headers
fmt.Fprintf(file, "\nHeaders:\n")
_, err = fmt.Fprintf(file, "\nHeaders:\n")
if err != nil {
log.Error().Err(err).Msg("writing response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
for name, values := range r.Header {
for _, value := range values {
fmt.Fprintf(file, "%s: %s\n", name, value)
@ -320,13 +299,29 @@ func webhookFieldseeker(w http.ResponseWriter, r *http.Request) {
}
// Write body
fmt.Fprintf(file, "\nBody:\n")
_, err = fmt.Fprintf(file, "\nBody:\n")
if err != nil {
log.Error().Err(err).Msg("writing response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v", err)
fmt.Fprintf(file, "Error reading body: %v\n", err)
_, err = fmt.Fprintf(file, "Error reading body: %v\n", err)
if err != nil {
log.Error().Err(err).Msg("writing response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
} else {
file.Write(body)
_, err = file.Write(body)
if err != nil {
log.Error().Err(err).Msg("writing response")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if len(body) == 0 {
fmt.Fprintf(file, "(empty body)")
}
@ -348,3 +343,27 @@ func parseTime(x string) (*time.Time, error) {
created := time.UnixMilli(created_epoch)
return &created, nil
}
type about struct {
Environment string `json:"environment"`
SentryDSN string `json:"sentry_dsn"`
Tegola tegolaURLs `json:"tegola"`
Version version.VersionInfo `json:"version"`
}
type tegolaURLs struct {
Nidus string `json:"nidus"`
RMO string `json:"rmo"`
}
func getRoot(ctx context.Context, r *http.Request, q resource.QueryParams) (*about, *nhttp.ErrorWithStatus) {
v := version.Get()
return &about{
Environment: config.Environment,
SentryDSN: config.SentryDSNFrontend,
Tegola: tegolaURLs{
Nidus: config.MakeURLTegola("/maps/nidus/{z}/{x}/{y}?id={organization_id}"),
RMO: config.MakeURLTegola("/maps/rmo/{z}/{x}/{y}"),
},
Version: v,
}, nil
}

93
api/audio.go Normal file
View file

@ -0,0 +1,93 @@
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/background"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
func apiAudioPost(w http.ResponseWriter, r *http.Request, u platform.User) {
vars := mux.Vars(r)
id := vars["uuid"]
noteUUID, err := uuid.Parse(id)
if err != nil {
http.Error(w, "Failed to decode the uuid", http.StatusBadRequest)
return
}
var payload NoteAudioPayload
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read the payload", http.StatusBadRequest)
return
}
if err := json.Unmarshal(body, &payload); err != nil {
//debugSaveRequest(body, err, "Audio note POST JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
ctx := r.Context()
setter := models.NoteAudioSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(int32(u.ID)),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Duration: omit.From(payload.Duration),
OrganizationID: omit.From(u.Organization.ID),
Transcription: omitnull.FromPtr(payload.Transcription),
TranscriptionUserEdited: omit.From(payload.TranscriptionUserEdited),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
if err := platform.NoteAudioCreate(ctx, u, setter); err != nil {
renderShim(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiAudioContentPost(w http.ResponseWriter, r *http.Request, user platform.User) {
vars := mux.Vars(r)
u_str := vars["uuid"]
u, err := uuid.Parse(u_str)
if err != nil {
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
return
}
err = file.FileContentWrite(r.Body, file.CollectionAudioRaw, u)
if err != nil {
log.Printf("Failed to write content file: %v", err)
http.Error(w, "failed to write content file", http.StatusInternalServerError)
return
}
ctx := r.Context()
a, err := models.NoteAudios.Query(
models.SelectWhere.NoteAudios.UUID.EQ(u),
models.SelectWhere.NoteAudios.OrganizationID.EQ(user.Organization.ID),
).One(ctx, db.PGInstance.BobDB)
if err != nil {
log.Printf("Failed to get note audio %s for org %d: %w", u_str, user.Organization.ID, err)
http.Error(w, "failed to update database", http.StatusBadRequest)
return
}
err = background.NewAudioTranscode(ctx, db.PGInstance.BobDB, a.ID)
if err != nil {
log.Printf("Failed to transcode audio %s for org %d: %w", u_str, user.Organization.ID, err)
http.Error(w, "failed to transcode audio", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}

1
api/avatar.go Normal file
View file

@ -0,0 +1 @@
package api

1
api/communication.go Normal file
View file

@ -0,0 +1 @@
package api

109
api/compliance.go Normal file
View file

@ -0,0 +1,109 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/Gleipnir-Technology/bob"
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/gorilla/mux"
"github.com/paulmach/orb/geojson"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/scan"
)
func getComplianceRequestImagePool(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
code := vars["public_id"]
if code == "" {
http.Error(w, "empty public_id", http.StatusBadRequest)
return
}
ctx := r.Context()
/*
comp, err := models.ComplianceReportRequests.Query(
models.Preload.ComplianceReportRequest.Lead(),
models.SelectWhere.ComplianceReportRequests.PublicID.EQ(code),
).One(ctx, db.PGInstance.BobDB)
if err != nil {
http.Error(w, "no comp", http.StatusInternalServerError)
return
}
lead := comp.R.Lead
site := lead.R.Site
*/
type _Row struct {
Envelope string `db:"parcel_envelope"`
OrganizationID int32 `db:"organization_id"`
}
row, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
sm.Columns(
"ST_AsGeoJSON(ST_Envelope(parcel.geometry)) AS parcel_envelope",
"organization.id AS organization_id",
),
sm.From("compliance_report_request"),
sm.InnerJoin("lead").OnEQ(
psql.Quote("compliance_report_request.lead_id"),
psql.Quote("organization.id"),
),
sm.InnerJoin("organization").OnEQ(
psql.Quote("lead.organization_id"),
psql.Quote("organization.id"),
),
sm.InnerJoin("site").On(
psql.Quote("lead.site_id").EQ(psql.Quote("site.id")),
),
sm.InnerJoin("parcel").OnEQ(
psql.Quote("site.parcel_id"),
psql.Quote("parcel.id"),
),
sm.Where(psql.Quote("compliance_report_request").EQ(psql.Arg(code))),
), scan.StructMapper[_Row]())
org, err := platform.OrganizationByID(ctx, int(row.OrganizationID))
if err != nil {
http.Error(w, "org err", http.StatusInternalServerError)
return
}
if org == nil {
http.Error(w, "no org", http.StatusBadRequest)
return
}
var polygon geojson.Polygon
err = json.Unmarshal([]byte(row.Envelope), &polygon)
if err != nil {
log.Error().Err(err).Msg("unmarshal json")
http.Error(w, "unmarshal envelope json", http.StatusInternalServerError)
return
}
ring := polygon[0]
p := ring[0]
err = writeImage(ctx, w, *org, 19, p[1], p[0])
if err != nil {
log.Error().Err(err).Msg("write image")
http.Error(w, "failed to write image", http.StatusInternalServerError)
return
}
}
func writeImage(ctx context.Context, w http.ResponseWriter, org platform.Organization, level uint, lat, lng float64) error {
img, err := platform.ImageAtPoint(ctx, org, level, lat, lng)
if err != nil {
return fmt.Errorf("image at point: %w", err)
}
log.Info().Int("size", len(img.Content)).Msg("image")
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(img.Content)))
_, err = io.Copy(w, bytes.NewBuffer(img.Content))
if err != nil {
return fmt.Errorf("copy bytes: %w", err)
}
return nil
}

145
api/configuration.go Normal file
View file

@ -0,0 +1,145 @@
package api
import (
"context"
"net/http"
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/arcgis/model"
queryarcgis "github.com/Gleipnir-Technology/nidus-sync/db/query/arcgis"
"github.com/Gleipnir-Technology/nidus-sync/html"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/rs/zerolog/log"
)
type contentConfigurationRoot struct{}
func getConfigurationRoot(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentConfigurationRoot], *nhttp.ErrorWithStatus) {
return html.NewResponse("sync/configuration/root.html", contentConfigurationRoot{}), nil
}
type contentSettingOrganization struct {
Organization platform.Organization
}
type contentSettingIntegration struct {
ArcGISAccount *model.Account
ArcGISOAuth *model.OAuthToken
ServiceMaps []model.ServiceMap
}
func getConfigurationOrganization(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentSettingOrganization], *nhttp.ErrorWithStatus) {
/*
var district contentDistrict
district, err = bob.One[contentDistrict](ctx, db.PGInstance.BobDB, psql.Select(
sm.From("import.district"),
sm.Columns(
"address",
"agency",
"area_4326_sqm",
"city1",
"city2",
"contact",
"fax1",
"general_mg",
"gid",
"phone1",
"phone2",
"postal_c_1",
"website",
psql.F("ST_AsGeoJSON", "centroid_4326"),
psql.F("ST_XMin", "extent_4326"),
psql.F("ST_YMin", "extent_4326"),
psql.F("ST_XMax", "extent_4326"),
psql.F("ST_YMax", "extent_4326"),
),
sm.Where(psql.Quote("gid").EQ(psql.Arg(gid))),
), scan.StructMapper[contentDistrict]())
if err != nil {
respondError(w, "Failed to get extents", err, http.StatusInternalServerError)
return
}
*/
data := contentSettingOrganization{
Organization: u.Organization,
}
return html.NewResponse("sync/configuration/organization.html", data), nil
}
func getConfigurationIntegration(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentSettingIntegration], *nhttp.ErrorWithStatus) {
oauth, err := platform.GetOAuthForUser(ctx, u)
if err != nil {
return nil, nhttp.NewError("Failed to get oauth: %w", err)
}
data := contentSettingIntegration{
ArcGISOAuth: oauth,
}
return html.NewResponse("sync/configuration/integration.html", data), nil
}
func getConfigurationIntegrationArcgis(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentSettingIntegration], *nhttp.ErrorWithStatus) {
oauth, err := platform.GetOAuthForUser(ctx, u)
if err != nil {
return nil, nhttp.NewError("Failed to get oauth: %w", err)
}
var account model.Account
var service_maps []model.ServiceMap
account_id := u.Organization.ArcgisAccountID()
if account_id != "" {
account, err = queryarcgis.AccountFromID(ctx, account_id)
if err != nil {
return nil, nhttp.NewError("Failed to get arcgis: %w", err)
}
service_maps, err = queryarcgis.ServiceMapsFromAccountID(ctx, account.ID)
if err != nil {
return nil, nhttp.NewError("Failed to get map services: %w", err)
}
}
data := contentSettingIntegration{
ArcGISAccount: &account,
ArcGISOAuth: oauth,
ServiceMaps: service_maps,
}
return html.NewResponse("sync/configuration/integration-arcgis.html", data), nil
}
type contentSettingPlaceholder struct{}
func getConfigurationPesticide(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSettingPlaceholder], *nhttp.ErrorWithStatus) {
content := contentSettingPlaceholder{}
return html.NewResponse("sync/configuration/pesticide.html", content), nil
}
func getConfigurationPesticideAdd(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSettingPlaceholder], *nhttp.ErrorWithStatus) {
content := contentSettingPlaceholder{}
return html.NewResponse("sync/configuration/pesticide-add.html", content), nil
}
func getConfigurationUserAdd(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSettingPlaceholder], *nhttp.ErrorWithStatus) {
content := contentSettingPlaceholder{}
return html.NewResponse("sync/configuration/user-add.html", content), nil
}
func getConfigurationUserList(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSettingPlaceholder], *nhttp.ErrorWithStatus) {
content := contentSettingPlaceholder{}
return html.NewResponse("sync/configuration/user-list.html", content), nil
}
type formArcgisConfiguration struct {
MapService *string `schema:"map-service"`
}
func postConfigurationIntegrationArcgis(ctx context.Context, r *http.Request, u platform.User, f formArcgisConfiguration) (string, *nhttp.ErrorWithStatus) {
if f.MapService != nil {
_, err := psql.Update(
um.Table("organization"),
um.SetCol("arcgis_map_service_id").ToArg(f.MapService),
um.Where(psql.Quote("id").EQ(psql.Arg(u.Organization.ID))),
).Exec(ctx, db.PGInstance.BobDB)
if err != nil {
return "", nhttp.NewError("Failed to update map service config: %w", err)
}
log.Info().Str("map-service", *f.MapService).Int32("org-id", u.Organization.ID).Msg("changed map service")
} else {
log.Info().Msg("no map service")
}
return "/configuration/integration/arcgis", nil
}

View file

@ -1,19 +1,26 @@
package api
import (
"io"
"net/http"
"os"
"github.com/Gleipnir-Technology/nidus-sync/lint"
"github.com/rs/zerolog/log"
)
func debugSaveRequest(body []byte, err error, message string) {
// TODO(eliribble): avoid using a single static filename and instead securely generate
// this value
log.Error().Err(err).Msg(message)
output, err := os.OpenFile("/tmp/request.body", os.O_RDWR|os.O_CREATE, 0666)
func debugSaveRequest(r *http.Request) {
tmpFile, err := os.CreateTemp("/tmp", "request-*.data")
if err != nil {
log.Info().Msg("Failed to open temp request.bady")
log.Error().Err(err).Msg("failed to create temp file for debugSaveRequest")
return
}
defer output.Close()
output.Write(body)
log.Info().Msg("Wrote request to /tmp/request.body")
defer lint.LogOnErr(tmpFile.Close, "close temp file")
_, err = io.Copy(tmpFile, r.Body)
if err != nil {
log.Error().Err(err).Msg("failed to copy request body in debugSaveRequest")
return
}
log.Info().Str("filename", tmpFile.Name()).Msg("Saved request body")
}

82
api/district.go Normal file
View file

@ -0,0 +1,82 @@
package api
import (
"fmt"
"net/http"
"strconv"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/gorilla/mux"
)
func apiGetDistrict(w http.ResponseWriter, r *http.Request) {
var latStr, lngStr string
err := r.ParseForm()
if err != nil {
renderShim(w, r, errRender(fmt.Errorf("Failed to parse GET form: %w", err)))
return
} else {
latStr = r.FormValue("lat")
lngStr = r.FormValue("lng")
}
lat, err := strconv.ParseFloat(latStr, 64)
if err != nil {
renderShim(w, r, errRender(fmt.Errorf("Failed to parse lat as float: %w", err)))
return
}
lng, err := strconv.ParseFloat(lngStr, 64)
if err != nil {
renderShim(w, r, errRender(fmt.Errorf("Failed to parse lng as float: %w", err)))
return
}
org, err := platform.DistrictForLocation(r.Context(), lng, lat)
if err != nil {
renderShim(w, r, errRender(fmt.Errorf("Failed to get district: %w", err)))
return
}
if org == nil {
http.NotFound(w, r)
return
}
d := ResponseDistrict{
Agency: org.Name,
Manager: org.GeneralManagerName.GetOr(""),
Phone: org.OfficePhone.GetOr(""),
Website: org.Website.GetOr(""),
}
if err := renderShim(w, r, d); err != nil {
renderShim(w, r, errRender(err))
}
}
func apiGetDistrictLogo(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
slug := vars["slug"]
ctx := r.Context()
rows, err := models.Organizations.Query(
models.SelectWhere.Organizations.Slug.EQ(slug),
).All(ctx, db.PGInstance.BobDB)
if err != nil {
http.Error(w, "Failed to query", http.StatusInternalServerError)
return
}
switch len(rows) {
case 0:
http.Error(w, "Organization not found", http.StatusNotFound)
return
case 1:
org := rows[0]
if org.LogoUUID.IsNull() {
http.Error(w, "Logo not found", http.StatusNotFound)
return
}
file.ImageFileToWriter(file.CollectionLogo, org.LogoUUID.MustGet(), w)
return
default:
http.Error(w, "Too many organizations, this is a programmer error", http.StatusInternalServerError)
return
}
}

View file

@ -1,25 +0,0 @@
package api
import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/Gleipnir-Technology/nidus-sync/auth"
)
func AddRoutes(r chi.Router) {
// Authenticated endpoints
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Method("GET", "/mosquito-source", auth.NewEnsureAuth(apiMosquitoSource))
r.Method("GET", "/service-request", auth.NewEnsureAuth(apiServiceRequest))
r.Method("GET", "/trap-data", auth.NewEnsureAuth(apiTrapData))
r.Method("GET", "/client/ios", auth.NewEnsureAuth(handleClientIos))
r.Method("POST", "/audio/{uuid}", auth.NewEnsureAuth(apiAudioPost))
r.Method("POST", "/audio/{uuid}/content", auth.NewEnsureAuth(apiAudioContentPost))
r.Method("POST", "/image/{uuid}", auth.NewEnsureAuth(apiImagePost))
r.Method("POST", "/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentPost))
// Unauthenticated endpoints
r.Get("/webhook/fieldseeker", webhookFieldseeker)
r.Post("/webhook/fieldseeker", webhookFieldseeker)
}

168
api/event.go Normal file
View file

@ -0,0 +1,168 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
"github.com/Gleipnir-Technology/nidus-sync/version"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
var connectionsSSE map[*ConnectionSSE]bool = make(map[*ConnectionSSE]bool, 0)
var TYPE_STATUS string = "status"
type ConnectionSSE struct {
chanEvent chan platform.Event
id uuid.UUID
organizationID int32
userID int32
}
type Message struct {
Resource string `json:"resource"`
Time time.Time `json:"time"`
Type string `json:"type"`
URI string `json:"uri"`
}
type Status struct {
BuildTime time.Time `json:"build_time"`
IsModified bool `json:"is_modified"`
Revision string `json:"revision"`
Status string `json:"status"`
Type string `json:"type"`
}
func (c *ConnectionSSE) SendEvent(w http.ResponseWriter, m platform.Event) error {
if m.Type == event.EventTypeShutdown {
v := version.Get()
return send(w, Status{
BuildTime: v.BuildTime,
IsModified: v.IsModified,
Revision: v.Revision,
Status: m.Type.String(),
Type: TYPE_STATUS,
})
}
return send(w, Message{
Resource: m.Resource,
Time: m.Time,
Type: m.Type.String(),
URI: m.URI,
})
}
func (c *ConnectionSSE) SendHeartbeat(w http.ResponseWriter, t time.Time) error {
return send(w, platform.Event{
Resource: "clock",
Time: t,
Type: platform.EventTypeHeartbeat,
URI: "",
})
}
func SetEventChannel(chan_envelopes <-chan platform.Envelope) {
go func() {
for envelope := range chan_envelopes {
for conn := range connectionsSSE {
if conn.organizationID == envelope.OrganizationID || envelope.OrganizationID == 0 {
log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-org", envelope.OrganizationID).Msg("pushed event to client")
conn.chanEvent <- envelope.Event
} else if conn.userID == envelope.UserID {
log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-user", envelope.UserID).Msg("pushed event to user")
conn.chanEvent <- envelope.Event
} else {
log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-org", envelope.OrganizationID).Int32("conn-org", conn.organizationID).Msg("skipped event, bad org")
}
}
}
}()
}
func send[T any](w http.ResponseWriter, msg T) error {
jsonData, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("marshaling json: %w", err)
}
// Write in SSE format: "data: <json>\n\n"
_, err = fmt.Fprintf(w, "data: %s\n\n", jsonData)
if err != nil {
return fmt.Errorf("writing SSE message: %w", err)
}
w.(http.Flusher).Flush()
return nil
}
func streamEvents(w http.ResponseWriter, r *http.Request, u platform.User) {
// Set headers for SSE
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
uid, err := uuid.NewUUID()
if err != nil {
log.Error().Err(err).Msg("failed to create uuid")
http.Error(w, "failed to create uuid", http.StatusInternalServerError)
return
}
connection := ConnectionSSE{
chanEvent: make(chan platform.Event),
id: uid,
organizationID: u.Organization.ID,
userID: int32(u.ID),
}
connectionsSSE[&connection] = true
log.Debug().Int32("org", u.Organization.ID).Int("user", u.ID).Str("id", uid.String()).Msg("connected SSE client")
// Send an initial connected event
v := version.Get()
status := Status{
BuildTime: v.BuildTime,
IsModified: v.IsModified,
Revision: v.Revision,
Status: "connected",
Type: TYPE_STATUS,
}
body, err := json.Marshal(status)
if err != nil {
log.Error().Err(err).Msg("failed to marshal connect status")
http.Error(w, "failed to marshal connect status", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "data: %s\n\n", body)
w.(http.Flusher).Flush()
// Keep the connection open with a ticker sending periodic events
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// Use a channel to detect when the client disconnects
done := r.Context().Done()
// Keep connection open until client disconnects
for {
select {
case <-done:
log.Debug().Int32("org", u.Organization.ID).Int("user", u.ID).Str("id", uid.String()).Msg("Client closed connection")
delete(connectionsSSE, &connection)
return
case t := <-ticker.C:
// Send a heartbeat message
err = connection.SendHeartbeat(w, t)
if err != nil {
log.Error().Err(err).Msg("Failed to send heartbeat")
}
case e := <-connection.chanEvent:
err = connection.SendEvent(w, e)
if err != nil {
log.Error().Err(err).Msg("Failed to send heartbeat")
}
}
}
}

412
api/handler.go Normal file
View file

@ -0,0 +1,412 @@
package api
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/auth"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/Gleipnir-Technology/nidus-sync/resource"
"github.com/google/uuid"
"github.com/gorilla/schema"
"github.com/rs/zerolog/log"
)
type ErrorAPI struct {
Message string `json:"message"`
}
var decoder = schema.NewDecoder()
type handlerBase func(context.Context, http.ResponseWriter, *http.Request) *nhttp.ErrorWithStatus
type handlerBaseAuthenticated func(context.Context, http.ResponseWriter, *http.Request, platform.User) *nhttp.ErrorWithStatus
type handlerFunctionDelete func(context.Context, *http.Request, platform.User) *nhttp.ErrorWithStatus
type handlerFunctionGet[T any] func(context.Context, *http.Request, resource.QueryParams) (*T, *nhttp.ErrorWithStatus)
type handlerFunctionGetAuthenticated[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) (*T, *nhttp.ErrorWithStatus)
type handlerFunctionGetImage func(context.Context, *http.Request, platform.User) (file.Collection, uuid.UUID, *nhttp.ErrorWithStatus)
type handlerFunctionGetSlice[T any] func(context.Context, *http.Request, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus)
type handlerFunctionGetSliceAuthenticated[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) ([]T, *nhttp.ErrorWithStatus)
type handlerFunctionPost[RequestType any, ResponseType any] func(context.Context, *http.Request, RequestType) (ResponseType, *nhttp.ErrorWithStatus)
type handlerFunctionPostAuthenticated[RequestType any, ResponseType any] func(context.Context, *http.Request, platform.User, RequestType) (ResponseType, *nhttp.ErrorWithStatus)
type handlerFunctionPostFormMultipart[RequestType any, ResponseType any] func(context.Context, *http.Request, RequestType) (*ResponseType, *nhttp.ErrorWithStatus)
type handlerFunctionPutAuthenticated[RequestType any] func(context.Context, *http.Request, platform.User, RequestType) (string, *nhttp.ErrorWithStatus)
func authenticatedHandlerBasic(f handlerBaseAuthenticated) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
e := f(ctx, w, r, u)
if e != nil {
respondErrorStatus(w, e)
return
}
return
})
}
func authenticatedHandlerDelete(f handlerFunctionDelete) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
e := f(ctx, r, u)
if e != nil {
respondErrorStatus(w, e)
return
}
http.Error(w, "", http.StatusNoContent)
return
})
}
func authenticatedHandlerGetImage(f handlerFunctionGetImage) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
collection, uid, e := f(ctx, r, u)
if e != nil {
respondErrorStatus(w, e)
return
}
file.ImageFileToWriter(collection, uid, w)
})
}
func authenticatedHandlerJSON[T any](f handlerFunctionGetAuthenticated[T]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
var body []byte
var params resource.QueryParams
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("failed to decode query: %w", err))
return
}
resp, e := f(ctx, r, u, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
respondErrorStatus(w, e)
return
}
body, err = json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
_, err = w.Write(body)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to write json: %w", err))
return
}
})
}
func authenticatedHandlerJSONSlice[T any](f handlerFunctionGetSliceAuthenticated[T]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
var body []byte
var params resource.QueryParams
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("failed to decode query: %w", err))
return
}
resp, e := f(ctx, r, u, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
respondErrorStatus(w, e)
return
}
if resp == nil {
body, err = json.Marshal([]struct{}{})
} else {
body, err = json.Marshal(resp)
}
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
_, err = w.Write(body)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to write json: %w", err))
return
}
})
}
func authenticatedHandlerJSONPost[RequestType any, ResponseType any](f handlerFunctionPostAuthenticated[RequestType, ResponseType]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
w.Header().Set("Content-Type", "application/json")
req, e := parseRequest[RequestType](r)
if e != nil {
serializeError(w, e)
return
}
ctx := r.Context()
resp, e := f(ctx, r, u, *req)
if e != nil {
serializeError(w, e)
return
}
body, err := json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
_, err = w.Write(body)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to write json: %w", err))
return
}
})
}
func authenticatedHandlerJSONPut[RequestType any](f handlerFunctionPutAuthenticated[RequestType]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
w.Header().Set("Content-Type", "application/json")
req, e := parseRequest[RequestType](r)
if e != nil {
serializeError(w, e)
return
}
ctx := r.Context()
path, e := f(ctx, r, u, *req)
if e != nil {
serializeError(w, e)
return
}
if path == "" {
w.WriteHeader(http.StatusNoContent)
return
}
w.Header().Set("Location", path)
http.Redirect(w, r, path, http.StatusCreated)
})
}
func authenticatedHandlerPostMultipart[ResponseType any](f handlerFunctionPostAuthenticated[[]file.Upload, ResponseType], collection file.Collection) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
if err != nil {
respondError(w, http.StatusBadRequest, "Failed to parse form: %w ", err)
return
}
uploads, err := file.SaveFileUploads(r, collection)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to save uploads: %w", err)
return
}
/*
err = decoder.Decode(&content, r.PostForm)
if err != nil {
respondError(w, http.StatusBadRequest, "Failed to decode form: %w", err)
return
}
*/
ctx := r.Context()
resp, e := f(ctx, r, u, uploads)
if e != nil {
http.Error(w, e.Error(), e.Status)
return
}
body, err := json.Marshal(resp)
if err != nil {
log.Error().Err(err).Msg("failed to marshal json")
http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError)
return
}
w.Write(body)
})
}
func handlerBasic(f handlerBase) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
e := f(ctx, w, r)
if e != nil {
respondErrorStatus(w, e)
return
}
}
}
func handlerJSON[T any](f handlerFunctionGet[T]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var body []byte
var params resource.QueryParams
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("failed to decode query: %w", err))
return
}
resp, e := f(ctx, r, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
respondErrorStatus(w, e)
return
}
body, err = json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
}
}
func handlerJSONSlice[T any](f handlerFunctionGetSlice[T]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var body []byte
var params resource.QueryParams
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("failed to decode query: %w", err))
return
}
resp, e := f(ctx, r, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
respondErrorStatus(w, e)
return
}
body, err = json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
}
}
func handlerJSONPost[RequestType any, ResponseType any](f handlerFunctionPost[RequestType, ResponseType]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
req, e := parseRequest[RequestType](r)
if e != nil {
serializeError(w, e)
return
}
ctx := r.Context()
resp, e := f(ctx, r, *req)
if e != nil {
serializeError(w, e)
return
}
body, err := json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
}
}
func handlerJSONPut[RequestType any, ResponseType any](f handlerFunctionPost[RequestType, ResponseType]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
req, e := parseRequest[RequestType](r)
if e != nil {
serializeError(w, e)
return
}
ctx := r.Context()
resp, e := f(ctx, r, *req)
if e != nil {
serializeError(w, e)
return
}
body, err := json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
}
}
func handlerFormPost[RequestType any, ResponseType any](f handlerFunctionPostFormMultipart[RequestType, ResponseType]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := r.ParseMultipartForm(32 << 12) // 128 MB buffer
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("bad form: %w", err))
return
}
var req RequestType
err = decoder.Decode(&req, r.PostForm)
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("decode form: %w", err))
return
}
ctx := r.Context()
resp, e := f(ctx, r, req)
if e != nil {
serializeError(w, e)
return
}
body, err := json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
}
}
func parseRequest[RequestType any](r *http.Request) (*RequestType, *nhttp.ErrorWithStatus) {
var err error
var req RequestType
content_type := r.Header.Get("Content-Type")
switch content_type {
case "application/json":
body, e := io.ReadAll(r.Body)
if e != nil {
return nil, nhttp.NewError("Failed to read body: %w", err)
}
err = json.Unmarshal(body, &req)
case "application/x-www-form-urlencoded":
e := r.ParseForm()
if err != nil {
return nil, nhttp.NewBadRequest("parsing form: %w", e)
}
err = decoder.Decode(&req, r.PostForm)
default:
return nil, nhttp.NewBadRequest("unrecognized content type '%s'", content_type)
}
if err != nil {
return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Failed to decode request: %w", err)
}
return &req, nil
}
func serializeError(w http.ResponseWriter, e *nhttp.ErrorWithStatus) {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err := json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
return
}
func respondError(w http.ResponseWriter, status int, format string, args ...any) {
outer_err := fmt.Errorf(format, args...)
body, err := json.Marshal(ErrorAPI{
Message: outer_err.Error(),
})
if err != nil {
http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), status)
}
func respondErrorStatus(w http.ResponseWriter, e *nhttp.ErrorWithStatus) {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err := json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
}

84
api/image.go Normal file
View file

@ -0,0 +1,84 @@
package api
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
func apiImagePost(w http.ResponseWriter, r *http.Request, u platform.User) {
vars := mux.Vars(r)
id := vars["uuid"]
noteUUID, err := uuid.Parse(id)
if err != nil {
http.Error(w, "Failed to decode the uuid", http.StatusBadRequest)
return
}
var payload NoteImagePayload
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read the payload", http.StatusBadRequest)
return
}
if err := json.Unmarshal(body, &payload); err != nil {
//debugSaveRequest(body, err, "Image note POST JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
ctx := r.Context()
setter := models.NoteImageSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(int32(u.ID)),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
OrganizationID: omit.From(u.Organization.ID),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
err = platform.NoteImageCreate(ctx, u, setter)
if err != nil {
renderShim(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiImageContentGet(w http.ResponseWriter, r *http.Request, u platform.User) {
vars := mux.Vars(r)
u_str := vars["uuid"]
imageUUID, err := uuid.Parse(u_str)
if err != nil {
log.Error().Err(err).Msg("Failed to parse image UUID")
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
}
file.ImageFileToWriter(file.CollectionPublicImage, imageUUID, w)
w.WriteHeader(http.StatusOK)
}
func apiImageContentPost(w http.ResponseWriter, r *http.Request, u platform.User) {
vars := mux.Vars(r)
u_str := vars["uuid"]
imageUUID, err := uuid.Parse(u_str)
if err != nil {
log.Error().Err(err).Msg("Failed to parse image UUID")
http.Error(w, "Failed to parse image UUID", http.StatusBadRequest)
}
err = file.ImageFileFromReader(file.CollectionImageRaw, imageUUID, r.Body)
if err != nil {
renderShim(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusOK)
log.Printf("Saved image file %s\n", imageUUID)
fmt.Fprintf(w, "PNG uploaded successfully")
}

1
api/lead.go Normal file
View file

@ -0,0 +1 @@
package api

50
api/publicreport.go Normal file
View file

@ -0,0 +1,50 @@
package api
import (
"context"
"fmt"
"net/http"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
)
type formPublicreportSignal struct {
ReportID string `json:"reportID"`
}
func postPublicreportSignal(ctx context.Context, r *http.Request, user platform.User, req formPublicreportSignal) (string, *nhttp.ErrorWithStatus) {
signal_id, err := platform.SignalCreateFromPublicreport(ctx, user, req.ReportID)
if err != nil {
return "", nhttp.NewError("create signal: %w", err)
}
return fmt.Sprintf("/signal/%d", *signal_id), nil
}
type formPublicreportInvalid struct {
ReportID string `json:"reportID"`
}
func postPublicreportInvalid(ctx context.Context, r *http.Request, user platform.User, req formPublicreportSignal) (string, *nhttp.ErrorWithStatus) {
err := platform.PublicReportInvalid(ctx, user, req.ReportID)
if err != nil {
return "", nhttp.NewError("create signal: %w", err)
}
return fmt.Sprintf("/publicreport/%s", req.ReportID), nil
}
type formPublicreportMessage struct {
Message string `json:"message"`
ReportID string `json:"reportID"`
}
func postPublicreportMessage(ctx context.Context, r *http.Request, user platform.User, req formPublicreportMessage) (string, *nhttp.ErrorWithStatus) {
msg_id, err := platform.PublicReportMessageCreate(ctx, user, req.ReportID, req.Message)
if err != nil {
return "", nhttp.NewError("failed to create message: %s", err)
}
if msg_id == nil {
return "", nhttp.NewError("nil message id")
}
return fmt.Sprintf("/message/%d", *msg_id), nil
}

29
api/review.go Normal file
View file

@ -0,0 +1,29 @@
package api
import (
"context"
"errors"
"fmt"
"net/http"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
)
type createReviewPool struct {
Status string `json:"status"`
TaskID int32 `json:"task_id"`
Updates *platform.PoolUpdate `json:"updates"`
}
func postReviewPool(ctx context.Context, r *http.Request, user platform.User, req createReviewPool) (string, *nhttp.ErrorWithStatus) {
id, err := platform.ReviewPoolCreate(ctx, user, req.TaskID, req.Status, req.Updates)
if err != nil {
if errors.As(err, &platform.ErrorNotFound{}) {
return "", nhttp.NewErrorStatus(http.StatusNotFound, "review task %d not found", req.TaskID)
}
return "", nhttp.NewError("failed to set review: %w", err)
}
return fmt.Sprintf("/review/%d", id), nil
}

169
api/routes.go Normal file
View file

@ -0,0 +1,169 @@
package api
import (
"github.com/Gleipnir-Technology/nidus-sync/auth"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/Gleipnir-Technology/nidus-sync/resource"
"github.com/gorilla/mux"
)
func AddRoutesRMO(r *mux.Router) {
router := resource.NewRouter(r)
compliance_request := resource.ComplianceRequest(router)
district := resource.District(router)
geocode := resource.Geocode(router)
nuisance := resource.Nuisance(router)
pr_compliance := resource.PublicReportCompliance(router)
publicreport := resource.Publicreport(router)
publicreport_notification := resource.PublicreportNotification(router)
qrcode := resource.QRCode(router)
water := resource.Water(router)
r.HandleFunc("", handlerJSON(getRoot))
r.HandleFunc("/compliance-request/image/pool/{public_id}", compliance_request.ImagePoolGet).Methods("GET").Name("compliance-request.image.pool.ByIDGet")
r.Handle("/district", handlerJSONSlice(district.List)).Methods("GET")
r.Handle("/district/{id}", handlerJSON(district.GetByID)).Methods("GET").Name("district.ByIDGet")
r.HandleFunc("/district/{slug}/logo", apiGetDistrictLogo).Methods("GET").Name("district.logo.BySlug")
r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET")
r.Handle("/geocode/reverse", handlerJSONPost(geocode.Reverse)).Methods("POST")
r.Handle("/geocode/reverse/closest", handlerJSONPost(geocode.ReverseClosest)).Methods("POST")
r.Handle("/geocode/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET")
r.Handle("/publicreport-notification", handlerJSONPost(publicreport_notification.Create)).Methods("POST")
r.Handle("/qr-code/mailer/{code}", handlerBasic(qrcode.Mailer)).Methods("GET")
r.Handle("/qr-code/marketing", handlerBasic(qrcode.Marketing)).Methods("GET")
r.Handle("/qr-code/report/{code}", handlerBasic(qrcode.Report)).Methods("GET")
r.HandleFunc("/rmo/compliance", handlerJSONPost(pr_compliance.Create)).Methods("POST")
r.HandleFunc("/rmo/nuisance", handlerFormPost(nuisance.Create)).Methods("POST")
r.Handle("/rmo/publicreport/{id}", handlerBasic(publicreport.ByIDPublic)).Methods("GET").Name("publicreport.ByIDGetPublic")
r.Handle("/rmo/publicreport/compliance/{id}/image", handlerFormPost(publicreport.ImageCreate)).Methods("POST")
r.Handle("/rmo/publicreport/compliance/{id}", handlerJSON(pr_compliance.ByIDPublic)).Methods("GET").Name("publicreport.compliance.ByIDGetPublic")
r.Handle("/rmo/publicreport/compliance/{id}", handlerJSONPut(pr_compliance.Update)).Methods("PUT")
r.Handle("/rmo/publicreport/nuisance/{id}", handlerJSON(nuisance.ByIDPublic)).Methods("GET").Name("publicreport.nuisance.ByIDGetPublic")
r.Handle("/rmo/publicreport/water/{id}", handlerJSON(water.ByIDPublic)).Methods("GET").Name("publicreport.water.ByIDGetPublic")
r.Handle("/rmo/publicreport/{id}", handlerBasic(publicreport.ByIDPublic)).Methods("GET").Name("publicreport.ByIDGetPublicPublic")
r.HandleFunc("/rmo/water", handlerFormPost(water.Create)).Methods("POST")
}
func AddRoutesSync(r *mux.Router) {
router := resource.NewRouter(r)
compliance_request := resource.ComplianceRequest(router)
district := resource.District(router)
geocode := resource.Geocode(router)
lob_hook := resource.LobHook(router)
nuisance := resource.Nuisance(router)
pr_compliance := resource.PublicReportCompliance(router)
publicreport := resource.Publicreport(router)
publicreport_notification := resource.PublicreportNotification(router)
qrcode := resource.QRCode(router)
service_request := resource.ServiceRequest(router)
water := resource.Water(router)
//r.Use(render.SetContentType(render.ContentTypeJSON))
// Unauthenticated endpoints
r.HandleFunc("", handlerJSON(getRoot))
r.HandleFunc("/compliance-request/image/pool/{public_id}", compliance_request.ImagePoolGet).Methods("GET").Name("compliance-request.image.pool.ByIDGet")
r.Handle("/district", handlerJSONSlice(district.List)).Methods("GET")
r.Handle("/district/{id}", handlerJSON(district.GetByID)).Methods("GET").Name("district.ByIDGet")
r.HandleFunc("/district/{slug}/logo", apiGetDistrictLogo).Methods("GET").Name("district.logo.BySlug")
r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET")
r.Handle("/geocode/reverse", handlerJSONPost(geocode.Reverse)).Methods("POST")
r.Handle("/geocode/reverse/closest", handlerJSONPost(geocode.ReverseClosest)).Methods("POST")
r.Handle("/geocode/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET")
r.Handle("/lob/event", handlerBasic(lob_hook.Event)).Methods("POST")
r.Handle("/publicreport-notification", handlerJSONPost(publicreport_notification.Create)).Methods("POST")
r.Handle("/qr-code/mailer/{code}", handlerBasic(qrcode.Mailer)).Methods("GET")
r.Handle("/qr-code/marketing", handlerBasic(qrcode.Marketing)).Methods("GET")
r.Handle("/qr-code/report/{code}", handlerBasic(qrcode.Report)).Methods("GET")
r.HandleFunc("/signin", handlerJSONPost(postSignin))
r.Handle("/signout", authenticatedHandlerBasic(postSignout))
r.HandleFunc("/signup", handlerJSONPost(postSignup))
r.HandleFunc("/twilio/call", twilioCallPost).Methods("POST")
r.HandleFunc("/twilio/call/status", twilioCallStatusPost).Methods("POST")
r.HandleFunc("/twilio/message", twilioMessagePost).Methods("POST")
r.HandleFunc("/twilio/text", twilioTextPost).Methods("POST")
r.HandleFunc("/twilio/text/status", twilioTextStatusPost).Methods("POST")
r.HandleFunc("/voipms/text", voipmsTextGet).Methods("GET")
r.HandleFunc("/voipms/text", voipmsTextPost).Methods("POST")
r.HandleFunc("/webhook/fieldseeker", webhookFieldseeker).Methods("GET")
r.HandleFunc("/webhook/fieldseeker", webhookFieldseeker).Methods("POST")
// Authenticated endpoints
r.Handle("/audio/{uuid}", auth.NewEnsureAuth(apiAudioPost)).Methods("POST")
r.Handle("/audio/{uuid}/content", auth.NewEnsureAuth(apiAudioContentPost)).Methods("POST")
avatar := resource.Avatar(router)
r.Handle("/avatar/{uuid}", authenticatedHandlerGetImage(avatar.ByUUIDGet)).Methods("GET").Name("avatar.ByUUIDGet")
r.Handle("/avatar", authenticatedHandlerPostMultipart(avatar.Create, file.CollectionAvatar)).Methods("POST")
r.Handle("/client/ios", auth.NewEnsureAuth(handleClientIos)).Methods("GET")
communication := resource.Communication(router)
r.Handle("/communication", authenticatedHandlerJSONSlice(communication.List)).Methods("GET")
r.Handle("/communication/{id}", authenticatedHandlerJSON(communication.Get)).Methods("GET").Name("communication.ByIDGet")
r.Handle("/communication/{id}/mark/invalid", authenticatedHandlerJSONPost(communication.MarkInvalid)).Methods("POST").Name("communication.MarkInvalid")
r.Handle("/communication/{id}/mark/pending-response", authenticatedHandlerJSONPost(communication.MarkPendingResponse)).Methods("POST").Name("communication.MarkPendingResponse")
r.Handle("/communication/{id}/mark/possible-issue", authenticatedHandlerJSONPost(communication.MarkPossibleIssue)).Methods("POST").Name("communication.MarkPossibleIssue")
r.Handle("/communication/{id}/mark/possible-resolved", authenticatedHandlerJSONPost(communication.MarkPossibleResolved)).Methods("POST").Name("communication.MarkPossibleResolved")
r.Handle("/compliance-request/mailer", authenticatedHandlerJSONPost(compliance_request.CreateMailer)).Methods("POST")
//r.HandleFunc("/compliance-request/image/pool/{public_id}", getComplianceRequestImagePool).Methods("GET")
r.Handle("/configuration/integration/arcgis", authenticatedHandlerJSONPost(postConfigurationIntegrationArcgis)).Methods("POST")
r.Handle("/events", auth.NewEnsureAuth(streamEvents)).Methods("GET")
r.Handle("/image/{uuid}", auth.NewEnsureAuth(apiImagePost)).Methods("POST")
r.Handle("/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentGet)).Methods("GET")
r.Handle("/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentPost)).Methods("POST")
impersonation := resource.Impersonation(router)
r.Handle("/impersonation", authenticatedHandlerJSONPost(impersonation.Create)).Methods("POST")
r.Handle("/impersonation", authenticatedHandlerDelete(impersonation.Delete)).Methods("DELETE")
lead := resource.Lead(r)
r.Handle("/leads", authenticatedHandlerJSON(lead.List)).Methods("GET")
r.Handle("/leads", authenticatedHandlerJSONPost(lead.Create)).Methods("POST")
mailer := resource.Mailer(router)
r.Handle("/mailer", authenticatedHandlerJSONSlice(mailer.List)).Methods("GET")
r.Handle("/mailer/{id}", authenticatedHandlerJSONPost(mailer.ByIDGet)).Methods("GET").Name("mailer.ByIDGet")
r.Handle("/mosquito-source", auth.NewEnsureAuth(apiMosquitoSource)).Methods("GET")
r.Handle("/publicreport/invalid", authenticatedHandlerJSONPost(postPublicreportInvalid)).Methods("POST")
r.Handle("/publicreport/signal", authenticatedHandlerJSONPost(postPublicreportSignal)).Methods("POST")
r.Handle("/publicreport/message", authenticatedHandlerJSONPost(postPublicreportMessage)).Methods("POST")
r.Handle("/publicreport/{id}", authenticatedHandlerBasic(publicreport.ByID)).Methods("GET").Name("publicreport.ByIDGet")
r.Handle("/publicreport/compliance/{id}", authenticatedHandlerJSON(pr_compliance.ByID)).Methods("GET").Name("publicreport.compliance.ByIDGet")
r.Handle("/publicreport/nuisance/{id}", authenticatedHandlerJSON(nuisance.ByID)).Methods("GET").Name("publicreport.nuisance.ByIDGet")
r.Handle("/publicreport/water/{id}", authenticatedHandlerJSON(water.ByID)).Methods("GET").Name("publicreport.water.ByIDGet")
r.Handle("/publicreport-notification", handlerJSONPost(publicreport_notification.Create)).Methods("POST")
r.Handle("/review/pool", authenticatedHandlerJSONPost(postReviewPool)).Methods("POST")
review_task := resource.ReviewTask(r)
r.Handle("/review-task", authenticatedHandlerJSON(review_task.List)).Methods("GET")
r.Handle("/service-request", authenticatedHandlerJSONSlice(service_request.List)).Methods("GET")
session := resource.Session(router)
r.Handle("/session", authenticatedHandlerJSON(session.Get)).Methods("GET").Name("session.get")
signal := resource.Signal(r)
r.Handle("/signal", authenticatedHandlerJSON(signal.List)).Methods("GET")
site := resource.Site(router)
r.Handle("/site", authenticatedHandlerJSONSlice(site.List)).Methods("GET")
r.Handle("/site/{id}", authenticatedHandlerJSON(site.ByIDGet)).Methods("GET").Name("site.ByIDGet")
sync := resource.Sync(r)
r.Handle("/sync", authenticatedHandlerJSONSlice(sync.List)).Methods("GET")
r.Handle("/sudo/email", authenticatedHandlerJSONPost(postSudoEmail)).Methods("POST")
r.Handle("/sudo/sms", authenticatedHandlerJSONPost(postSudoSMS)).Methods("POST")
r.Handle("/sudo/sse", authenticatedHandlerJSONPost(postSudoSSE)).Methods("POST")
r.Handle("/trap-data", auth.NewEnsureAuth(apiTrapData)).Methods("GET")
r.Handle("/tile/{z}/{y}/{x}", auth.NewEnsureAuth(getTile)).Methods("GET")
upload := resource.Upload(r)
r.Handle("/upload/pool/custom", authenticatedHandlerPostMultipart(upload.PoolCustomCreate, file.CollectionCSV)).Methods("POST")
r.Handle("/upload/pool/flyover", authenticatedHandlerPostMultipart(upload.PoolFlyoverCreate, file.CollectionCSV)).Methods("POST")
r.Handle("/upload", authenticatedHandlerJSON(upload.List)).Methods("GET")
r.Handle("/upload/{id}", authenticatedHandlerJSON(upload.ByIDGet)).Methods("GET")
r.Handle("/upload/{id}/commit", authenticatedHandlerJSONPost(upload.Commit)).Methods("POST")
r.Handle("/upload/{id}/discard", authenticatedHandlerJSONPost(upload.Discard)).Methods("POST")
user := resource.User(router)
r.Handle("/user/self", authenticatedHandlerJSON(user.SelfGet)).Methods("GET")
r.Handle("/user/suggestion", authenticatedHandlerJSON(user.SuggestionGet)).Methods("GET")
r.Handle("/user", authenticatedHandlerJSONSlice(user.List)).Methods("GET")
r.Handle("/user/{id}", authenticatedHandlerJSON(user.ByIDGet)).Methods("GET").Name("user.ByIDGet")
r.Handle("/user/{id}", authenticatedHandlerJSONPut(user.ByIDPut)).Methods("PUT")
// Unauthenticated endpoints
}

1
api/signal.go Normal file
View file

@ -0,0 +1 @@
package api

46
api/signin.go Normal file
View file

@ -0,0 +1,46 @@
package api
import (
"context"
"errors"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/auth"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/rs/zerolog/log"
)
type reqSignin struct {
Password string `schema:"password"`
Username string `schema:"username"`
}
func postSignin(ctx context.Context, r *http.Request, req reqSignin) (string, *nhttp.ErrorWithStatus) {
if req.Password == "" {
return "", nhttp.NewBadRequest("Empty password")
}
if req.Username == "" {
return "", nhttp.NewBadRequest("Empty username")
}
log.Info().Str("username", req.Username).Msg("API Signin")
_, err := auth.SigninUser(r, req.Username, req.Password)
if err != nil {
if errors.Is(err, auth.InvalidCredentials{}) {
return "", nhttp.NewUnauthorized("invalid credentials")
}
if errors.Is(err, auth.InvalidUsername{}) {
return "", nhttp.NewUnauthorized("invalid credentials")
}
if errors.Is(err, platform.NoUserError{}) {
return "", nhttp.NewUnauthorized("invalid credentials")
}
log.Error().Err(err).Str("username", req.Username).Msg("Login server error")
return "", nhttp.NewError("login server error")
}
return "/", nil
}
func postSignout(ctx context.Context, w http.ResponseWriter, r *http.Request, u platform.User) *nhttp.ErrorWithStatus {
auth.SignoutUser(r, u)
return nil
}

37
api/signup.go Normal file
View file

@ -0,0 +1,37 @@
package api
import (
"context"
"net/http"
"strings"
"github.com/Gleipnir-Technology/nidus-sync/auth"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/rs/zerolog/log"
)
type reqSignup struct {
Username string `json:"username"`
Name string `json:"name"`
Password string `json:"password"`
Terms bool `json:"terms"`
}
func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string, *nhttp.ErrorWithStatus) {
log.Info().Str("username", signup.Username).Str("name", signup.Name).Str("password", strings.Repeat("*", len(signup.Password))).Msg("Signup")
if !signup.Terms {
log.Warn().Msg("Terms not agreed")
return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must agree to the terms to register")
}
user, err := auth.SignupUser(r.Context(), signup.Username, signup.Name, signup.Password)
if err != nil {
return "", nhttp.NewError("Failed to signup user", err)
}
auth.AddUserSession(ctx, user)
return "/", nil
}

104
api/sudo.go Normal file
View file

@ -0,0 +1,104 @@
package api
import (
"context"
"fmt"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/comms/email"
"github.com/Gleipnir-Technology/nidus-sync/comms/text"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/html"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/rs/zerolog/log"
)
type contentSudo struct {
ForwardEmailRMOAddress string
ForwardEmailNidusAddress string
}
func getSudo(ctx context.Context, r *http.Request, user platform.User) (*html.Response[contentSudo], *nhttp.ErrorWithStatus) {
if !user.HasRoot() {
return nil, &nhttp.ErrorWithStatus{
Message: "You have to be a root user to access this",
Status: http.StatusForbidden,
}
}
content := contentSudo{
ForwardEmailRMOAddress: config.ForwardEmailRMOAddress,
ForwardEmailNidusAddress: config.ForwardEmailNidusAddress,
}
return html.NewResponse("sync/sudo.html", content), nil
}
type FormEmail struct {
Body string `schema:"emailBody"`
From string `schema:"emailFrom"`
Subject string `schema:"emailSubject"`
To string `schema:"emailTo"`
}
func postSudoEmail(ctx context.Context, r *http.Request, u platform.User, e FormEmail) (string, *nhttp.ErrorWithStatus) {
if !u.HasRoot() {
return "", &nhttp.ErrorWithStatus{
Message: "You must have sudo powers to do this",
Status: http.StatusForbidden,
}
}
request := email.Request{
From: e.From,
HTML: fmt.Sprintf("<html><p>%s</p></html>", e.Body),
Sender: e.From,
Subject: e.Subject,
To: e.To,
Text: e.Body,
}
resp, err := email.Send(ctx, request)
if err != nil {
log.Warn().Err(err).Msg("Failed to send email")
} else {
log.Info().Str("id", resp.ID).Str("to", e.To).Msg("Sent Email")
}
return "/sudo", nil
}
type FormSMS struct {
Message string `schema:"smsMessage"`
Phone string `schema:"smsPhone"`
}
func postSudoSMS(ctx context.Context, r *http.Request, u platform.User, sms FormSMS) (string, *nhttp.ErrorWithStatus) {
if !u.HasRoot() {
return "", &nhttp.ErrorWithStatus{
Message: "You must have sudo powers to do this",
Status: http.StatusForbidden,
}
}
id, err := text.SendText(ctx, config.VoipMSNumber, sms.Phone, sms.Message)
if err != nil {
log.Warn().Err(err).Msg("Failed to send SMS")
} else {
log.Info().Str("id", id).Msg("Sent SMS")
}
return "/sudo", nil
}
type FormSSE struct {
OrganizationID int32 `schema:"organizationID"`
Resource string `schema:"resource"`
Type string `schema:"type"`
URIPath string `schema:"uriPath"`
}
func postSudoSSE(ctx context.Context, r *http.Request, u platform.User, sse FormSSE) (string, *nhttp.ErrorWithStatus) {
if !u.HasRoot() {
return "", &nhttp.ErrorWithStatus{
Message: "You must have sudo powers to do this",
Status: http.StatusForbidden,
}
}
platform.SudoEvent(sse.OrganizationID, sse.Resource, sse.Type, sse.URIPath)
return "/sudo", nil
}

38
api/tile.go Normal file
View file

@ -0,0 +1,38 @@
package api
import (
"net/http"
"strconv"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/gorilla/mux"
//"github.com/rs/zerolog/log"
)
func getTile(w http.ResponseWriter, r *http.Request, user platform.User) {
vars := mux.Vars(r)
x_str := vars["x"]
y_str := vars["y"]
z_str := vars["z"]
x, err := strconv.Atoi(x_str)
if err != nil {
http.Error(w, "can't parse x as an integer", http.StatusBadRequest)
return
}
y, err := strconv.Atoi(y_str)
if err != nil {
http.Error(w, "can't parse x as an integer", http.StatusBadRequest)
return
}
z, err := strconv.Atoi(z_str)
if err != nil {
http.Error(w, "can't parse x as an integer", http.StatusBadRequest)
return
}
err = platform.GetTile(r.Context(), w, user.Organization, true, uint(z), uint(y), uint(x))
if err != nil {
http.Error(w, "failed to do tile", http.StatusInternalServerError)
return
}
}

158
api/twilio.go Normal file
View file

@ -0,0 +1,158 @@
package api
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/platform/text"
"github.com/rs/zerolog/log"
"github.com/twilio/twilio-go/twiml"
)
// Translate from Twilio's representation of a RCS message sender to our concept of a phone number
// From: rcs:dev_report_mosquitoes_online_dosrvwxm_agent
// To: +16235525879
func getDst(to string) (string, error) {
if to == config.TwilioRCSSenderRMO {
return config.PhoneNumberReportStr, nil
}
/*
phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, to)
if err != nil {
return "", fmt.Errorf("Failed to search for dest phone %s: %w", to, err)
}
return phone.E164, nil
*/
return "", fmt.Errorf("Cannot match phone number to '%s'", to)
}
func splitPhoneSource(s string) (string, string) {
parts := strings.Split(s, ":")
switch len(parts) {
case 0:
return "this isn't", "possible"
case 1:
return "", s
case 2:
return parts[0], parts[1]
default:
log.Warn().Str("s", s).Msg("Got an incomprehensible number of parts of a phone number")
return parts[0], parts[1]
}
}
func twilioMessagePost(w http.ResponseWriter, r *http.Request) {
message_sid := r.PostFormValue("MessageSid")
log.Info().Str("sid", message_sid).Msg("Twilio Message POST")
fmt.Fprintf(w, "")
}
func twilioCallPost(w http.ResponseWriter, r *http.Request) {
called := r.PostFormValue("Called")
tostate := r.PostFormValue("ToState")
callercountry := r.PostFormValue("CallerCountry")
direction := r.PostFormValue("Direction")
callerstate := r.PostFormValue("CallerState")
tozip := r.PostFormValue("ToZip")
callsid := r.PostFormValue("CallSid")
to := r.PostFormValue("To")
callerzip := r.PostFormValue("CallerZip")
tocountry := r.PostFormValue("ToCountry")
stirverstat := r.PostFormValue("StirVerstat")
//calltoken := r.PostFormValue("CallToken")
calledzip := r.PostFormValue("CalledZip")
apiversion := r.PostFormValue("ApiVersion")
calledcity := r.PostFormValue("CalledCity")
callstatus := r.PostFormValue("CallStatus")
from := r.PostFormValue("From")
accountsid := r.PostFormValue("AccountSid")
calledcountry := r.PostFormValue("CalledCountry")
callercity := r.PostFormValue("CallerCity")
tocity := r.PostFormValue("ToCity")
fromcountry := r.PostFormValue("FromCountry")
caller := r.PostFormValue("Caller")
fromcity := r.PostFormValue("FromCity")
calledstate := r.PostFormValue("CalledState")
fromzip := r.PostFormValue("FromZip")
fromstate := r.PostFormValue("FromState")
log.Info().Str("called", called).Str("tostate", tostate).Str("callercountry", callercountry).Str("direction", direction).Str("callerstate", callerstate).Str("tozip", tozip).Str("callsid", callsid).Str("to", to).Str("callerzip", callerzip).Str("tocountry", tocountry).Str("stirverstat", stirverstat).Str("calledzip", calledzip).Str("apiversion", apiversion).Str("calledcity", calledcity).Str("callstatus", callstatus).Str("from", from).Str("accountsid", accountsid).Str("calledcountry", calledcountry).Str("callercity", callercity).Str("tocity", tocity).Str("fromcountry", fromcountry).Str("caller", caller).Str("fromcity", fromcity).Str("calledstate", calledstate).Str("fromzip", fromzip).Str("fromstate", fromstate).Msg("Incoming phone call")
say := &twiml.VoiceSay{
Message: "Thanks for calling Report Mosquitoes Online. I'll forward you to our tech support lead, Eli",
}
call := &twiml.VoiceDial{
Number: config.PhoneNumberSupportStr,
}
twimlResult, err := twiml.Voice([]twiml.Element{say, call})
if err != nil {
log.Error().Err(err).Msg("Failed to produce TWIML")
http.Error(w, err.Error(), http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "text/xml")
fmt.Fprintf(w, "%s", twimlResult)
}
func twilioCallStatusPost(w http.ResponseWriter, r *http.Request) {
call_sid := r.PostFormValue("CallSid")
account_sid := r.PostFormValue("AccountSid")
from := r.PostFormValue("From")
to := r.PostFormValue("To")
call_status := r.PostFormValue("CallStatus")
api_version := r.PostFormValue("ApiVersion")
direction := r.PostFormValue("Direction")
forwarded_from := r.PostFormValue("ForwardedFrom")
caller_name := r.PostFormValue("CallerName")
parent_call_sid := r.PostFormValue("ParentCallSid")
log.Info().Str("call_sid", call_sid).Str("account_sid", account_sid).Str("from", from).Str("to", to).Str("call_status", call_status).Str("api_version", api_version).Str("direction", direction).Str("forwarded_from", forwarded_from).Str("caller_name", caller_name).Str("parent_call_sid", parent_call_sid)
fmt.Fprintf(w, "")
}
func twilioTextPost(w http.ResponseWriter, r *http.Request) {
message_sid := r.PostFormValue("MessageSid")
account_sid := r.PostFormValue("AccountSid")
messaging_service_sid := r.PostFormValue("MessagingServiceSid")
from := r.PostFormValue("From")
to_ := r.PostFormValue("To")
body := r.PostFormValue("Body")
num_media := r.PostFormValue("NumMedia")
num_segments := r.PostFormValue("NumSegments")
media_content_type0 := r.PostFormValue("MediaContentType0")
media_url0 := r.PostFormValue("MediaUrl0")
from_city := r.PostFormValue("FromCity")
from_state := r.PostFormValue("FromState")
from_zip := r.PostFormValue("FromZip")
from_country := r.PostFormValue("FromCountry")
to_city := r.PostFormValue("ToCity")
to_state := r.PostFormValue("ToState")
to_zip := r.PostFormValue("ToZip")
to_country := r.PostFormValue("ToCountry")
type_, src := splitPhoneSource(from)
log.Info().Str("message_sid", message_sid).Str("account_sid", account_sid).Str("messaging_service_sid", messaging_service_sid).Str("from", from).Str("to_", to_).Str("body", body).Str("num_media", num_media).Str("num_segments", num_segments).Str("media_content_type0", media_content_type0).Str("media_url0", media_url0).Str("from_city", from_city).Str("from_state", from_state).Str("from_zip", from_zip).Str("from_country", from_country).Str("to_city", to_city).Str("to_state", to_state).Str("to_zip", to_zip).Str("to_country", to_country).Str("type_", type_).Msg("got text")
twiml, _ := twiml.Messages([]twiml.Element{})
dst, err := getDst(to_)
if err != nil {
log.Error().Err(err).Str("to", to_).Msg("Failed to get dst")
return
}
go func() {
err := text.HandleTextMessage(context.Background(), src, dst, body)
if err != nil {
log.Error().Err(err).Msg("failed to handle Twilio incoming text")
}
}()
w.Header().Set("Content-Type", "text/xml")
fmt.Fprintf(w, "%s", twiml)
}
func twilioTextStatusPost(w http.ResponseWriter, r *http.Request) {
message_sid := r.PostFormValue("MessageSid")
message_status := r.PostFormValue("MessageStatus")
log.Info().Str("sid", message_sid).Str("status", message_status).Msg("Updated message status")
text.UpdateMessageStatus(message_sid, message_status)
fmt.Fprintf(w, "")
}

View file

@ -5,9 +5,12 @@ import (
"time"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/aarondl/opt/null"
"github.com/go-chi/render"
//"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
type H3Cell uint64
@ -32,13 +35,6 @@ func NewBounds() Bounds {
}
}
/* not sure if used
type Location struct {
Latitude float64
Longitude float64
}
*/
type NoteImagePayload struct {
UUID string `json:"uuid"`
Cell H3Cell `json:"cell"`
@ -62,6 +58,13 @@ type NoteAudioPayload struct {
Version int32 `json:"version"`
}
type ResponseDistrict struct {
Agency string `json:"agency"`
Manager string `json:"manager"`
Phone string `json:"phone"`
Website string `json:"website"`
}
type ResponseMosquitoSource struct {
Access string `json:"access"`
Active *bool `json:"active"`
@ -89,11 +92,10 @@ type NoteAudioBreadcrumbPayload struct {
type ResponseFieldseeker struct {
MosquitoSources []ResponseMosquitoSource `json:"sources"`
ServiceRequests []ResponseServiceRequest `json:"requests"`
ServiceRequests []types.ServiceRequest `json:"requests"`
TrapData []ResponseTrapData `json:"traps"`
}
// ResponseErr renderer type for handling all sorts of errors.
type ResponseClientIos struct {
Fieldseeker ResponseFieldseeker `json:"fieldseeker"`
Since time.Time `json:"since"`
@ -103,23 +105,6 @@ func (i ResponseClientIos) Render(w http.ResponseWriter, r *http.Request) error
return nil
}
// In the best case scenario, the excellent github.com/pkg/errors package
// helps reveal information on the error, setting it on Err, and in the Render()
// method, using it to set the application-specific error code in AppCode.
type ResponseErr struct {
Error error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code
StatusText string `json:"status"` // user-level status message
AppCode int64 `json:"code,omitempty"` // application-specific error code
ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
}
func (e *ResponseErr) Render(w http.ResponseWriter, r *http.Request) error {
render.Status(r, e.HTTPStatusCode)
return nil
}
type ResponseMosquitoInspection struct {
ActionTaken string `json:"action_taken"`
Comments string `json:"comments"`
@ -154,19 +139,28 @@ func NewResponseMosquitoInspections(inspections models.FieldseekerMosquitoinspec
return results
}
func (rd ResponseDistrict) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (rtd ResponseMosquitoSource) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
func NewResponseMosquitoSource(ms platform.MosquitoSource) ResponseMosquitoSource {
pl := ms.PointLocation
h3cell, err := h3utils.ToCell(pl.H3cell.GetOr("0"))
if err != nil {
log.Warn().Err(err).Msg("Failed to convert h3 cell")
h3cell = 0
}
return ResponseMosquitoSource{
Active: toBool16(pl.Active),
Access: pl.Accessdesc.GetOr(""),
Comments: pl.Comments.GetOr(""),
Created: formatTime(pl.Creationdate),
Description: pl.Description.GetOr(""),
//H3Cell: pl.H3Cell,
Active: toBool16(pl.Active),
Access: pl.Accessdesc.GetOr(""),
Comments: pl.Comments.GetOr(""),
Created: formatTime(pl.Creationdate),
Description: pl.Description.GetOr(""),
H3Cell: int64(h3cell),
ID: pl.Globalid.String(),
LastInspectionDate: formatTime(pl.Lastinspectdate),
Habitat: pl.Habitat.GetOr(""),
@ -241,48 +235,10 @@ func (rtd ResponseNote) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
type ResponseServiceRequest struct {
Address string `json:"address"`
AssignedTechnician string `json:"assigned_technician"`
City string `json:"city"`
Created string `json:"created"`
H3Cell int64 `json:"h3cell"`
HasDog *bool `json:"has_dog"`
HasSpanishSpeaker *bool `json:"has_spanish_speaker"`
ID string `json:"id"`
Priority string `json:"priority"`
RecordedDate string `json:"recorded_date"`
Source string `json:"source"`
Status string `json:"status"`
Target string `json:"target"`
Zip string `json:"zip"`
}
func (srr ResponseServiceRequest) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
func NewResponseServiceRequest(sr *models.FieldseekerServicerequest) ResponseServiceRequest {
return ResponseServiceRequest{
Address: sr.Reqaddr1.GetOr(""),
AssignedTechnician: sr.Assignedtech.GetOr(""),
City: sr.Reqcity.GetOr(""),
Created: formatTime(sr.Creationdate),
//H3Cell: sr.H3Cell,
HasDog: toBool(sr.Dog),
HasSpanishSpeaker: toBool(sr.Spanish),
ID: sr.Globalid.String(),
Priority: sr.Priority.GetOr(""),
Status: sr.Status.GetOr(""),
Source: sr.Source.GetOr(""),
Target: sr.Reqtarget.GetOr(""),
Zip: sr.Reqzip.GetOr(""),
}
}
func NewResponseServiceRequests(requests models.FieldseekerServicerequestSlice) []ResponseServiceRequest {
results := make([]ResponseServiceRequest, 0)
func NewResponseServiceRequests(requests models.FieldseekerServicerequestSlice) []types.ServiceRequest {
results := make([]types.ServiceRequest, 0)
for _, i := range requests {
results = append(results, NewResponseServiceRequest(i))
results = append(results, types.ServiceRequestFromModel(i))
}
return results
}

1
api/upload.go Normal file
View file

@ -0,0 +1 @@
package api

1
api/user.go Normal file
View file

@ -0,0 +1 @@
package api

105
api/voipms.go Normal file
View file

@ -0,0 +1,105 @@
package api
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
//"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/platform/text"
"github.com/rs/zerolog/log"
)
/*
{
"data": {
"id": 101252305,
"event_type": "message.received",
"record_type": "event",
"payload": {
"id": 101252305,
"record_type": "message",
"from": {
"phone_number": "+18016984649"
},
"to": [
{
"phone_number": "+15593720139",
"status": "webhook_delivered"
}
],
"text": "test3",
"received_at": "2026-01-29T20:16:23.000000+00:00",
"type": "SMS",
"media": []
}
}
}
*/
type VoipMSStatusPhoneFrom struct {
PhoneNumber string `json:"phone_number"`
}
type VoipMSStatusPhoneTo struct {
PhoneNumber string `json:"phone_number"`
Status string `json:"status"`
}
type VoipMSStatusPayload struct {
ID int `json:"id"`
RecordType string `json:"record_type"`
From VoipMSStatusPhoneFrom `json:"from"`
To []VoipMSStatusPhoneTo `json:"to"`
Text string `json:"text"`
ReceivedAt string `json:"received_at"`
Type string `json:"type"`
//Media []something
}
type VoipMSStatusUpdate struct {
ID int `json:"id"`
EventType string `json:"event_type"`
RecordType string `json:"record_type"`
Payload VoipMSStatusPayload `json:"payload"`
}
type VoipMSTextPostBody struct {
Data VoipMSStatusUpdate `json:"data"`
}
func voipmsTextGet(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
name := query.Get("to")
age := query.Get("from")
message := query.Get("message")
files := query.Get("files")
id := query.Get("id")
date := query.Get("date")
log.Info().Str("name", name).Str("age", age).Str("message", message).Str("files", files).Str("id", id).Str("date", date).Msg("Incoming text message")
}
func voipmsTextPost(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read", http.StatusInternalServerError)
return
}
//debugSaveRequest(r)
var b VoipMSTextPostBody
err = json.Unmarshal(body, &b)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
to := "unknown"
if len(b.Data.Payload.To) > 0 {
to = b.Data.Payload.To[0].PhoneNumber
}
log.Info().Int("ID", b.Data.ID).Str("event_type", b.Data.EventType).Str("record_type", b.Data.RecordType).Str("from", b.Data.Payload.From.PhoneNumber).Str("to", to).Str("content", b.Data.Payload.Text).Msg("Text status")
// Convert phone numbers from Voip.ms into E164 format for consistency
go func() {
err := text.HandleTextMessage(context.Background(), b.Data.Payload.From.PhoneNumber, to, b.Data.Payload.Text)
if err != nil {
log.Error().Err(err).Msg("failed to handle VoIP.ms incoming text")
}
}()
fmt.Fprintf(w, "ok")
}

@ -1 +1 @@
Subproject commit af786fabcc08ed506a23718a71aa0dd52ce047ac
Subproject commit 63cc8b573739294ea98f7e39d2baec3cd70dfd7f

View file

@ -6,27 +6,16 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/db/sql"
"github.com/Gleipnir-Technology/nidus-sync/debug"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/bob/dialect/psql"
"github.com/stephenafamo/bob/dialect/psql/sm"
"golang.org/x/crypto/bcrypt"
)
type NoCredentialsError struct{}
type InactiveUser struct{}
func (e NoCredentialsError) Error() string { return "No credentials were present in the request" }
type NoUserError struct{}
func (e NoUserError) Error() string { return "That user does not exist" }
func (e InactiveUser) Error() string { return "That user is not active" }
type InvalidCredentials struct{}
@ -36,30 +25,74 @@ type InvalidUsername struct{}
func (e InvalidUsername) Error() string { return "That username doesn't exist" }
type AuthenticatedHandler func(http.ResponseWriter, *http.Request, *models.User)
type NoCredentialsError struct{}
func (e NoCredentialsError) Error() string { return "No credentials were present in the request" }
type AuthenticatedHandler func(http.ResponseWriter, *http.Request, platform.User)
type EnsureAuth struct {
handler AuthenticatedHandler
}
func AddUserSession(r *http.Request, user *models.User) {
id := strconv.Itoa(int(user.ID))
sessionManager.Put(r.Context(), "user_id", id)
sessionManager.Put(r.Context(), "username", user.Username)
log.Info().Str("username", user.Username).Str("user_id", id).Msg("Created new user session")
func AddUserSession(ctx context.Context, user *platform.User) {
id_str := strconv.Itoa(int(user.ID))
sessionManager.Put(ctx, "user_id", id_str)
sessionManager.Put(ctx, "username", user.Username)
log.Debug().Str("id", id_str).Str("username", user.Username).Msg("added user session")
}
func ImpersonateEnd(ctx context.Context) {
sessionManager.Put(ctx, "impersonated_user_id", "")
}
func ImpersonateUser(ctx context.Context, target_user_id int) {
target_user_id_str := strconv.Itoa(int(target_user_id))
sessionManager.Put(ctx, "impersonated_user_id", target_user_id_str)
}
func ImpersonatedUser(ctx context.Context) *int32 {
i_str := sessionManager.GetString(ctx, "impersonated_user_id")
if i_str == "" {
return nil
}
i, err := strconv.Atoi(i_str)
if err != nil {
log.Error().Err(err).Str("impersonated_user_id", i_str).Msg("failed to parse impersonated_user_id")
return nil
}
result := int32(i)
return &result
}
func ImpersonatorID(ctx context.Context) *int32 {
user_id_str := sessionManager.GetString(ctx, "user_id")
user_id, err := strconv.Atoi(user_id_str)
if err != nil {
log.Error().Err(err).Str("user_id", user_id_str).Msg("failed to parse user_id")
return nil
}
result := int32(user_id)
return &result
func GetAuthenticatedUser(r *http.Request) (*models.User, error) {
//user_id := sessionManager.GetInt(r.Context(), "user_id")
user_id_str := sessionManager.GetString(r.Context(), "user_id")
}
func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
ctx := r.Context()
user_id_str := sessionManager.GetString(ctx, "user_id")
impersonated_user_id_str := sessionManager.GetString(ctx, "impersonated_user_id")
if impersonated_user_id_str != "" {
user_id_str = impersonated_user_id_str
}
if user_id_str != "" {
user_id, err := strconv.Atoi(user_id_str)
if err != nil {
return nil, fmt.Errorf("Failed to convert user_id to int: %w", err)
}
username := sessionManager.GetString(r.Context(), "username")
log.Info().Int("user_id", user_id).Str("username", username).Msg("Current session info")
username := sessionManager.GetString(ctx, "username")
if user_id > 0 && username != "" {
return findUser(r.Context(), user_id)
user, err := platform.UserByID(ctx, int32(user_id))
if err != nil {
return nil, fmt.Errorf("user by ID: %w", err)
}
if !user.IsActive {
return nil, fmt.Errorf("user is inactive")
}
return user, nil
}
}
// If we can't get the user from the session try to get from auth headers
@ -67,11 +100,11 @@ func GetAuthenticatedUser(r *http.Request) (*models.User, error) {
if !ok {
return nil, &NoCredentialsError{}
}
user, err := validateUser(r.Context(), username, password)
user, err := validateUser(ctx, username, password)
if err != nil {
return nil, err
}
AddUserSession(r, user)
AddUserSession(ctx, user)
return user, nil
}
@ -81,39 +114,44 @@ func NewEnsureAuth(handlerToWrap AuthenticatedHandler) *EnsureAuth {
func (ea *EnsureAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If this is an API request respond with a more machine-readable error state
accept := r.Header.Values("Accept")
offers := []string{"application/json", "text/html"}
accept := r.Header.Get("Accept")
/*
offers := []string{"application/json", "text/html"}
content_type := NegotiateContent(accept, offers)
content_type := NegotiateContent(accept, offers)
*/
user, err := GetAuthenticatedUser(r)
if err != nil || user == nil {
var msg []byte
// Separate return codes for different authentication failures
if _, ok := err.(*NoCredentialsError); ok {
fmt.Println("No credentials present and no session")
w.Header().Set("WWW-Authenticate-Error", "no-credentials")
msg = []byte("Please provide credentials.\n")
} else if _, ok := err.(*NoUserError); ok {
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
msg = []byte("Invalid credentials provided.\n")
} else if _, ok := err.(*InvalidCredentials); ok {
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
msg = []byte("Invalid credentials provided.\n")
// Don't send authentication headers for browsers because it forces the authentication popup
requested_with := r.Header.Get("X-Requested-With")
//log.Debug().Str("x-requested-with", requested_with).Send()
if !strings.HasPrefix(requested_with, "nidus-web") && accept != "text/event-stream" {
w.Header().Set("WWW-Authenticate", `Basic realm="Nidus Sync"`)
// Separate return codes for different authentication failures
if _, ok := err.(*NoCredentialsError); ok {
log.Info().Msg("No credentials present and no session")
w.Header().Set("WWW-Authenticate-Error", "no-credentials")
msg = []byte("Please provide credentials.\n")
} else if _, ok := err.(*platform.NoUserError); ok {
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
msg = []byte("Invalid credentials provided.\n")
} else if _, ok := err.(*InvalidCredentials); ok {
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
msg = []byte("Invalid credentials provided.\n")
}
}
if content_type == "text/html" {
http.Redirect(w, r, "/signin?next="+r.URL.Path, http.StatusSeeOther)
return
}
w.Header().Set("WWW-Authenticate", `Basic realm="Nidus Sync"`)
w.WriteHeader(401)
w.Write(msg)
_, err = w.Write(msg)
if err != nil {
log.Error().Err(err).Msg("failed to write response")
}
return
}
ea.handler(w, r, user)
ea.handler(w, r, *user)
}
func SigninUser(r *http.Request, username string, password string) (*models.User, error) {
func SigninUser(r *http.Request, username string, password string) (*platform.User, error) {
user, err := validateUser(r.Context(), username, password)
if err != nil {
return nil, err
@ -121,105 +159,76 @@ func SigninUser(r *http.Request, username string, password string) (*models.User
if user == nil {
return nil, errors.New("No matching user")
}
AddUserSession(r, user)
AddUserSession(r.Context(), user)
return user, nil
}
func SignupUser(ctx context.Context, username string, name string, password string) (*models.User, error) {
passwordHash, err := hashPassword(password)
func SignoutUser(r *http.Request, user platform.User) {
sessionManager.Put(r.Context(), "user_id", "")
sessionManager.Put(r.Context(), "username", "")
err := sessionManager.Destroy(r.Context())
if err != nil {
log.Error().Err(err).Msg("failed to destroy session for user on signout")
}
log.Info().Str("username", user.Username).Int("user_id", (user.ID)).Msg("Ended user session")
}
func SignupUser(ctx context.Context, username string, name string, password string) (*platform.User, error) {
password_hash, err := HashPassword(password)
if err != nil {
return nil, fmt.Errorf("Cannot signup user, failed to create hashed password: %w", err)
}
o_setter := models.OrganizationSetter{
Name: omitnull.From(fmt.Sprintf("%s's organization", username)),
ArcgisID: omitnull.From(""),
ArcgisName: omitnull.From(""),
FieldseekerURL: omitnull.From(""),
}
o, err := models.Organizations.Insert(&o_setter).One(ctx, db.PGInstance.BobDB)
u, err := platform.CreateUser(ctx, username, name, password_hash)
if err != nil {
return nil, fmt.Errorf("Failed to create organization: %w", err)
return nil, fmt.Errorf("create user: %s", err)
}
log.Info().Int32("id", o.ID).Msg("Created organization")
u_setter := models.UserSetter{
DisplayName: omit.From(name),
OrganizationID: omit.From(o.ID),
PasswordHash: omit.From(passwordHash),
PasswordHashType: omit.From(enums.HashtypeBcrypt14),
Username: omit.From(username),
}
u, err := models.Users.Insert(&u_setter).One(ctx, db.PGInstance.BobDB)
if err != nil {
return nil, fmt.Errorf("Failed to create user: %w", err)
}
log.Info().Int32("id", u.ID).Str("username", u.Username).Msg("Created user")
return u, nil
}
// Helper function to translate strings into solid error types for operating on
func findUser(ctx context.Context, user_id int) (*models.User, error) {
//user, err := models.FindUser(ctx, db.PGInstance.BobDB, int32(user_id))
user, err := models.Users.Query(
models.Preload.User.Organization(),
sm.Where(models.Users.Columns.ID.EQ(psql.Arg(user_id))),
).One(ctx, db.PGInstance.BobDB)
if err != nil {
if err.Error() == "No such user" || err.Error() == "sql: no rows in result set" {
return nil, &NoUserError{}
} else {
debug.LogErrorTypeInfo(err)
log.Error().Err(err).Msg("Unrecognized error. This should be updated in the findUser code")
return nil, err
}
}
log.Info().Int32("user_id", user.ID).Int32("org_id", user.OrganizationID).Msg("Found user")
return user, err
}
func hashPassword(password string) (string, error) {
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func redact(s string) string {
if len(s) <= 4 {
return s
}
first_two := s[:2]
last_two := s[len(s)-2:]
middle_length := len(s) - 4
return first_two + strings.Repeat("*", middle_length) + last_two
}
func validatePassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
if err != nil {
log.Debug().Err(err).Str("password", password).Str("hash", hash).Msg("!validate password")
}
return err == nil
}
func validateUser(ctx context.Context, username string, password string) (*models.User, error) {
passwordHash, err := hashPassword(password)
func validateUser(ctx context.Context, username string, password string) (*platform.User, error) {
passwordHash, err := HashPassword(password)
if err != nil {
return nil, fmt.Errorf("Failed to hash password: %w", err)
}
log.Info().Str("username", username).Str("password", password).Str("hash", passwordHash).Msg("Validating user")
result, err := sql.UserByUsername(username).All(ctx, db.PGInstance.BobDB)
user, err := platform.UserByUsername(ctx, username)
if err != nil {
return nil, fmt.Errorf("Failed to query for user: %w", err)
}
switch len(result) {
case 0:
if user == nil {
log.Info().Str("username", username).Str("password", redact(password)).Msg("Invalid username")
return nil, InvalidUsername{}
case 1:
row := result[0]
if !validatePassword(password, row.PasswordHash) {
return nil, InvalidCredentials{}
}
user := models.User{
ID: row.ID,
ArcgisAccessToken: row.ArcgisAccessToken,
ArcgisLicense: row.ArcgisLicense,
ArcgisRefreshToken: row.ArcgisRefreshToken,
ArcgisRefreshTokenExpires: row.ArcgisRefreshTokenExpires,
ArcgisRole: row.ArcgisRole,
DisplayName: row.DisplayName,
Email: row.Email,
OrganizationID: row.OrganizationID,
Username: row.Username,
}
return &user, nil
default:
return nil, errors.New("More than one matching row, this should be impossible.")
}
if !user.IsActive {
return nil, InactiveUser{}
}
if !validatePassword(password, user.PasswordHash) {
log.Info().Str("username", username).Str("password", redact(password)).Str("hash", passwordHash).Msg("Invalid password for user")
return nil, InvalidCredentials{}
}
return user, nil
}

View file

@ -3,9 +3,9 @@ package auth
import (
"time"
"github.com/alexedwards/scs/v2"
"github.com/alexedwards/scs/pgxstore"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/alexedwards/scs/pgxstore"
"github.com/alexedwards/scs/v2"
)
var sessionManager *scs.SessionManager

File diff suppressed because it is too large Load diff

35
cmd/passwordgen/main.go Normal file
View file

@ -0,0 +1,35 @@
package main
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"github.com/Gleipnir-Technology/nidus-sync/auth"
)
func main() {
var password string
scanValue("Please enter your password : ", &password)
hash, err := auth.HashPassword(password)
if err != nil {
fmt.Printf("Failed to hash password: %v\n", err)
os.Exit(1)
}
fmt.Println("Password:", password)
fmt.Println("Hash: ", hash)
}
func scanValue(message string, result *string) {
fmt.Print("%s", message)
scanner := bufio.NewScanner(os.Stdin)
if ok := scanner.Scan(); !ok {
log.Fatal(errors.New("Failed to scan input"))
}
*result = scanner.Text()
}

View file

@ -1,113 +0,0 @@
package main
import (
"context"
"os"
"time"
fslayer "github.com/Gleipnir-Technology/arcgis-go/fieldseeker/layer"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/alexedwards/scs/v2"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var sessionManager *scs.SessionManager
var BaseURL, ClientID, ClientSecret, Environment, FieldseekerSchemaDirectory, MapboxToken string
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
ClientID = os.Getenv("ARCGIS_CLIENT_ID")
if ClientID == "" {
log.Error().Msg("You must specify a non-empty ARCGIS_CLIENT_ID")
os.Exit(1)
}
ClientSecret = os.Getenv("ARCGIS_CLIENT_SECRET")
if ClientSecret == "" {
log.Error().Msg("You must specify a non-empty ARCGIS_CLIENT_SECRET")
os.Exit(1)
}
BaseURL = os.Getenv("BASE_URL")
if BaseURL == "" {
log.Error().Msg("You must specify a non-empty BASE_URL")
os.Exit(1)
}
bind := os.Getenv("BIND")
if bind == "" {
bind = ":9001"
}
Environment = os.Getenv("ENVIRONMENT")
if Environment == "" {
log.Error().Msg("You must specify a non-empty ENVIRONMENT")
os.Exit(1)
}
if !(Environment == "PRODUCTION" || Environment == "DEVELOPMENT") {
log.Error().Str("ENVIRONMENT", Environment).Msg("ENVIRONMENT should be either DEVELOPMENT or PRODUCTION")
os.Exit(2)
}
MapboxToken = os.Getenv("MAPBOX_TOKEN")
if MapboxToken == "" {
log.Error().Msg("You must specify a non-empty MAPBOX_TOKEN")
os.Exit(1)
}
pg_dsn := os.Getenv("POSTGRES_DSN")
if pg_dsn == "" {
log.Error().Msg("You must specify a non-empty POSTGRES_DSN")
os.Exit(1)
}
FieldseekerSchemaDirectory = os.Getenv("FIELDSEEKER_SCHEMA_DIRECTORY")
if FieldseekerSchemaDirectory == "" {
log.Error().Msg("You must specify a non-empty FIELDSEEKER_SCHEMA_DIRECTORY")
os.Exit(1)
}
log.Info().Msg("Starting...")
err := db.InitializeDatabase(context.TODO(), pg_dsn)
if err != nil {
log.Error().Str("err", err.Error()).Msg("Failed to connect to database")
os.Exit(2)
}
ctx := context.Background()
row := fslayer.RodentLocation{
ObjectID: 1,
LocationName: "some location",
Zone: "",
Zone2: "",
//Habitat: fslayer.RodentLocationRodentLocationHabitatCommercial,
//Priority: fslayer.RodentLocationLocationPriority1None,
//Usetype: fslayer.RodentLocationLocationUseType1Residential,
//Active: fslayer.RodentLocationNotInUITF1True,
Description: "",
Accessdesc: "",
Comments: "",
//Symbology: fslayer.RodentLocationRodentLocationSymbologyActionrequired,
ExternalID: "",
Nextactiondatescheduled: time.Now(),
Locationnumber: 1,
LastInspectionDate: time.Now(),
LastInspectionSpecies: "",
LastInspectionAction: "",
LastInspectionConditions: "",
LastInspectionRodentEvidence: "",
GlobalID: uuid.New(),
CreatedUser: "",
CreatedDate: time.Now(),
LastEditedUser: "",
LastEditedDate: time.Now(),
CreationDate: time.Now(),
Creator: "",
EditDate: time.Now(),
Editor: "",
Jurisdiction: "",
}
err = db.TestPreparedQuery(ctx, &row)
if err != nil {
log.Error().Str("err", err.Error()).Msg("Failed to run prepared query")
os.Exit(3)
}
log.Info().Msg("Complete.")
}

53
cmd/test-jet/main.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"context"
"log"
"os"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/query/public"
)
func main() {
err := config.Parse()
if err != nil {
log.Printf("failed on config: %v", err)
os.Exit(1)
}
ctx := context.TODO()
err = db.InitializeDatabase(ctx, config.PGDSN)
if err != nil {
log.Printf("failed on db: %v", err)
os.Exit(2)
}
txn, err := db.BeginTxn(ctx)
if err != nil {
log.Printf("failed on txn: %v", err)
os.Exit(3)
}
defer txn.Rollback(ctx)
log.Printf("doing address")
gid := "openaddresses:address:us/ca/tulare-addresses-county:0dc28458fd03e3fa"
address, err := public.AddressFromGID(ctx, txn, gid)
if err != nil {
log.Printf("failed on query: %v", err)
os.Exit(4)
}
//log.Printf("address %d lat %f lng %f", address.ID, *address.LocationLatitude, *address.LocationLongitude)
log.Printf("Address id %d location %s", address.ID, address.Location)
txn.Commit(ctx)
/*
log.Printf("doing comm")
id := int64(1)
comm, err := public.CommunicationFromID(ctx, id)
if err != nil {
log.Printf("failed on query: %v", err)
os.Exit(4)
}
log.Printf("communication %d", comm.ID)
*/
}

102
comms/email/email.go Normal file
View file

@ -0,0 +1,102 @@
package email
import (
"context"
"fmt"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/rs/zerolog/log"
"resty.dev/v3"
)
type attachmentRequest struct {
Filename string `json:"filename"`
Content string `json:"content"`
}
type Request struct {
From string `json:"from"`
To string `json:"to"`
CC []string `json:"cc,omitempty"`
BCC []string `json:"bcc,omitempty"`
Subject string `json:"subject"`
Text string `json:"text"`
HTML string `json:"html,omitempty"`
Attachments []attachmentRequest `json:"attachments,omitempty"`
Sender string `json:"sender"`
ReplyTo string `json:"replyTo,omitempty"`
InReplyTo string `json:"inReplyTo,omitempty"`
References []string `json:"references,omitempty"`
}
type emailEnvelope struct {
From string `json:"from"`
To []string `json:"to"`
}
type emailResponseError struct {
StatusCode int `json:"statusCode"`
Error string `json:"error"`
Message string `json:"message"`
}
type emailResponse struct {
IsRedacted bool `json:"is_redacted"`
CreatedAt string `json:"created_at"`
HardBounces []string `json:"hard_bounces"`
SoftBounces []string `json:"soft_bounces"`
IsBounce bool `json:"is_bounce"`
Alias string `json:"alias"`
Domain string `json:"domain"`
User string `json:"user"`
Status string `json:"status"`
IsLocked bool `json:"is_locked"`
Envelope emailEnvelope `json:"envelope"`
RequireTLS bool `json:"requireTLS"`
MessageID string `json:"messageId"`
Headers map[string]string `json:"headers"`
Date string `json:"date"`
Subject string `json:"subject"`
Accepted []string `json:"accepted"`
Deliveries []string `json:"deliveries"`
RejectedErrors []string `json:"rejectedErrors"`
ID string `json:"id"`
Object string `json:"object"`
UpdatedAt string `json:"updated_at"`
Link string `json:"link"`
Message string `json:"message"`
}
var FORWARDEMAIL_EMAIL_POST_API = "https://api.forwardemail.net/v1/emails"
func Send(ctx context.Context, email Request) (result emailResponse, err error) {
client := resty.New()
var err_resp emailResponseError
r, err := client.R().
SetBasicAuth(config.ForwardEmailAPIToken, "").
SetBody(email).
SetContext(ctx).
SetError(&err_resp).
SetHeader("Content-Type", "application/json").
SetResult(&result).
Post(FORWARDEMAIL_EMAIL_POST_API)
if err != nil {
return result, fmt.Errorf("Failed to marshal email request: %w", err)
}
if r.IsError() {
log.Error().
Int("status", err_resp.StatusCode).
Str("error", err_resp.Error).
Str("msg", err_resp.Message).
Str("email.from", email.From).
Str("email.sender", email.Sender).
Str("email.subject", email.Subject).
Str("email.to", email.To).
Str("email.text", email.Text).
Msg("Email send error")
return result, fmt.Errorf("Error response %d from email service: %s (%s)", err_resp.StatusCode, err_resp.Message, err_resp.Error)
}
return result, nil
}

62
comms/email/websocket.go Normal file
View file

@ -0,0 +1,62 @@
package email
import (
"context"
"errors"
"fmt"
"time"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
)
var FORWARDEMAIL_WS_API = "wss://api.forwardemail.net/v1/ws"
func StartWebsocket(ctx context.Context, api_token string) {
var conn *websocket.Conn
for {
err := ensureConnected(conn, api_token)
if err != nil {
log.Error().Err(err).Msg("Bailing on email websocket")
return
}
select {
case <-ctx.Done():
return
default:
// Read message
message_type, message, err := conn.ReadMessage()
if err != nil {
if !websocket.IsCloseError(err, websocket.CloseNormalClosure) {
conn = nil
}
log.Error().Err(err).Msg("Error reading message")
}
// Process and log the message
log.Info().Int("message_type", message_type).Bytes("message", message).Msg("Got email notification")
}
}
}
func ensureConnected(conn *websocket.Conn, api_token string) error {
if conn != nil {
return nil
}
url := FORWARDEMAIL_WS_API + "?token=" + api_token
for {
new_conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err == nil {
log.Info().Msg("Connected to mail websocket")
*conn = *new_conn
return nil
}
if errors.Is(err, websocket.ErrBadHandshake) {
return fmt.Errorf("Bad handshake connecting to email websocket, bailing.")
}
log.Error().Err(err).Str("url", url).Msg("Error connecting to WebSocket")
time.Sleep(3 * time.Second)
}
}

18
comms/text/text.go Normal file
View file

@ -0,0 +1,18 @@
package text
import (
"context"
"fmt"
"github.com/Gleipnir-Technology/nidus-sync/config"
)
func SendText(ctx context.Context, source string, destination string, message string) (string, error) {
switch config.TextProvider {
case "voipms":
return sendTextVoipms(ctx, destination, message)
case "twilio":
return sendTextTwilio(ctx, source, destination, message)
}
return "", fmt.Errorf("Unsupported provider '%s'", config.TextProvider)
}

52
comms/text/twilio.go Normal file
View file

@ -0,0 +1,52 @@
package text
import (
"context"
"encoding/json"
"fmt"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/rs/zerolog/log"
"github.com/twilio/twilio-go"
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
)
func sendTextTwilio(ctx context.Context, source string, destination string, message string) (string, error) {
client := twilio.NewRestClient()
params := &twilioApi.CreateMessageParams{}
params.SetMessagingServiceSid(config.TwilioMessagingServiceSID)
params.SetBody(message)
params.SetTo(destination)
resp, err := client.Api.CreateMessage(params)
if err != nil {
return "", fmt.Errorf("Failed to create message to %s: %w", destination, err)
}
if resp.Sid == nil {
log.Warn().Str("src", source).Str("dst", destination).Msg("Text message sid is nil")
return "", nil
}
log.Info().Str("src", source).Str("dst", destination).Str("message", message).Str("sid", *resp.Sid).Msg("Created text message")
return *resp.Sid, nil
}
func sendSMSTwilio(destination, source, message string) error {
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: config.TwilioAccountSID,
Password: config.TwilioAuthToken,
})
params := &twilioApi.CreateMessageParams{}
params.SetTo("+15558675309")
params.SetFrom("+15017250604")
params.SetBody("Hello from Go!")
resp, err := client.Api.CreateMessage(params)
if err != nil {
return fmt.Errorf("Error sending SMS message: %w", err)
}
response, _ := json.Marshal(*resp)
log.Debug().Str("response", string(response)).Msg("Send SMS")
return nil
}

108
comms/text/voipms.go Normal file
View file

@ -0,0 +1,108 @@
package text
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/lint"
"github.com/rs/zerolog/log"
)
var VOIP_MS_API = "https://voip.ms/api/v1/rest.php"
type VoipMSResponse struct {
MMS int `json:"mms"`
Message string `json:"message"`
Status string `json:"status"`
SMS int `json:"sms"`
}
func sendTextVoipms(ctx context.Context, to string, content string, media ...string) (string, error) {
if len(content) > 2048 {
return "", errors.New("Message content is more than 160 characters")
}
params := url.Values{}
params.Add("api_password", config.VoipMSPassword)
params.Add("api_username", config.VoipMSUsername)
params.Add("method", "sendMMS")
params.Add("did", config.VoipMSNumber)
params.Add("dst", to)
params.Add("message", content)
/*
for i, med := range media {
// These should be one of:
// 1. A full URL that the service cat GET
// 2. A base64-encoded image starting with "data:image/png;base64,iVBORw0KGgoAAAANSUh..."
params.Add(fmt.Sprintf("media%d", i+1), med)
}
params.Add(fmt.Sprintf("media%d", len(media)+1), "")
*/
response, err := makeVoipMSRequest(params)
if err != nil {
return "", fmt.Errorf("Failed to send MMS: %w", err)
}
if response.Status == "ip_not_enabled" {
return "", fmt.Errorf("Failed to send SMS: the IP address of the server is not enabled with voip.ms. You'll need to enable this server's IP with them.")
}
log.Info().Str("status", response.Status).Int("mms", response.MMS).Msg("Sent MMS message")
return strconv.Itoa(response.MMS), nil
}
func sendSMSVoipms(to string, content string) (string, error) {
if len(content) > 160 {
return "", errors.New("Message content is more than 160 characters")
}
params := url.Values{}
params.Add("api_password", config.VoipMSPassword)
params.Add("api_username", config.VoipMSUsername)
params.Add("method", "sendSMS")
params.Add("did", config.VoipMSNumber)
params.Add("dst", to)
params.Add("message", content)
response, err := makeVoipMSRequest(params)
if err != nil {
return "", fmt.Errorf("Failed to send SMS: %w", err)
}
log.Info().Str("status", response.Status).Int("sms", response.SMS).Msg("Sent MMS message")
return strconv.Itoa(response.SMS), nil
}
func makeVoipMSRequest(params url.Values) (VoipMSResponse, error) {
result := VoipMSResponse{}
// Construct the URL with query parameters
full_url := VOIP_MS_API + "?" + params.Encode()
// Make the HTTP request
log.Debug().Str("full_url", full_url).Msg("Sending command to VoIP.ms")
resp, err := http.Get(full_url)
if err != nil {
log.Warn().Err(err).Str("url", full_url).Msg("Failed to make request to Voip.MS")
return result, fmt.Errorf("Error making request: %w", err)
}
defer lint.LogOnErr(resp.Body.Close, "failed closing response body")
// Read the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Warn().Err(err).Str("url", full_url).Msg("Failed to read Voip.MS response body")
return result, fmt.Errorf("Failed to read response: %w", err)
}
log.Info().Str("response", string(body)).Msg("Response from Voip.MS")
// Parse the JSON response
var response VoipMSResponse
err = json.Unmarshal(body, &response)
if err != nil {
return result, fmt.Errorf("Failed to unmarshal JSON response: %w", err)
}
return response, nil
}

View file

@ -4,45 +4,76 @@ import (
"fmt"
"net/url"
"os"
"strconv"
"github.com/nyaruka/phonenumbers"
//"github.com/rs/zerolog/log"
)
var Bind, ClientID, ClientSecret, Environment, FieldseekerSchemaDirectory, MapboxToken, PGDSN, URLReport, URLSync, FilesDirectoryPublic, FilesDirectoryUser string
// Build the ArcGIS authorization URL with PKCE
func BuildArcGISAuthURL(clientID string) string {
baseURL := "https://www.arcgis.com/sharing/rest/oauth2/authorize/"
params := url.Values{}
params.Add("client_id", clientID)
params.Add("redirect_uri", RedirectURL())
params.Add("response_type", "code")
//params.Add("code_challenge", generateCodeChallenge(codeVerifier))
//params.Add("code_challenge_method", "S256")
// See https://developers.arcgis.com/rest/users-groups-and-items/token/
// expiration is defined in minutes
var expiration int
if IsProductionEnvironment() {
// 2 weeks is the maximum allowed
expiration = 20160
} else {
expiration = 20
}
params.Add("expiration", strconv.Itoa(expiration))
return baseURL + "?" + params.Encode()
}
var (
Bind string
ClientID string
ClientSecret string
DomainRMO string
DomainNidus string
DomainTegola string
Environment string
FilesDirectory string
FieldseekerSchemaDirectory string
ForwardEmailAPIToken string
ForwardEmailRMOAddress string
ForwardEmailRMOPassword string
ForwardEmailRMOUsername string
ForwardEmailNidusAddress string
ForwardEmailNidusPassword string
ForwardEmailNidusUsername string
LobAPIKey string
PGDSN string
PhoneNumberReport phonenumbers.PhoneNumber
PhoneNumberReportStr string
PhoneNumberSupport phonenumbers.PhoneNumber
PhoneNumberSupportStr string
SentryDSN string
SentryDSNFrontend string
StadiaMapsAPIKey string
TextProvider string
TwilioAuthToken string
TwilioAccountSID string
TwilioMessagingServiceSID string
TwilioRCSSenderRMO string
VoipMSNumber string
VoipMSPassword string
VoipMSUsername string
)
func IsProductionEnvironment() bool {
return Environment == "PRODUCTION"
}
func MakeURLSync(path string) string {
return fmt.Sprintf("https://%s%s", URLSync, path)
func makeURL(domain, path string, args ...string) string {
to_add := make([]any, 0)
for _, a := range args {
to_add = append(to_add, url.QueryEscape(a))
}
pattern := "https://" + domain + path
return fmt.Sprintf(pattern, to_add...)
}
func Parse() error {
func MakeURLNidus(path string, args ...string) string {
return makeURL(DomainNidus, path, args...)
}
func MakeURLReport(path string, args ...string) string {
return makeURL(DomainRMO, path, args...)
}
func MakeURLTegola(path string, args ...string) string {
//log.Debug().Str("path", path).Strs("args", args).Str("domain", DomainTegola).Msg("Making tegola url")
return makeURL(DomainTegola, path, args...)
}
func Parse() (err error) {
Bind = os.Getenv("BIND")
if Bind == "" {
Bind = ":9001"
}
ClientID = os.Getenv("ARCGIS_CLIENT_ID")
if ClientID == "" {
return fmt.Errorf("You must specify a non-empty ARCGIS_CLIENT_ID")
@ -51,48 +82,142 @@ func Parse() error {
if ClientSecret == "" {
return fmt.Errorf("You must specify a non-empty ARCGIS_CLIENT_SECRET")
}
URLReport = os.Getenv("URL_REPORT")
if URLReport == "" {
return fmt.Errorf("You must specify a non-empty URL_REPORT")
DomainNidus = os.Getenv("DOMAIN_NIDUS")
if DomainNidus == "" {
return fmt.Errorf("You must specify a non-empty DOMAIN_NIDUS")
}
URLSync = os.Getenv("URL_SYNC")
if URLSync == "" {
return fmt.Errorf("You must specify a non-empty URL_SYNC")
DomainRMO = os.Getenv("DOMAIN_RMO")
if DomainRMO == "" {
return fmt.Errorf("You must specify a non-empty DOMAIN_RMO")
}
Bind = os.Getenv("BIND")
if Bind == "" {
Bind = ":9001"
DomainTegola = os.Getenv("DOMAIN_TEGOLA")
if DomainTegola == "" {
return fmt.Errorf("You must specify a non-empty DOMAIN_TEGOLA")
}
Environment = os.Getenv("ENVIRONMENT")
if Environment == "" {
return fmt.Errorf("You must specify a non-empty ENVIRONMENT")
}
if !(Environment == "PRODUCTION" || Environment == "DEVELOPMENT") {
if Environment != "PRODUCTION" && Environment != "DEVELOPMENT" {
return fmt.Errorf("ENVIRONMENT should be either DEVELOPMENT or PRODUCTION")
}
MapboxToken = os.Getenv("MAPBOX_TOKEN")
if MapboxToken == "" {
return fmt.Errorf("You must specify a non-empty MAPBOX_TOKEN")
}
PGDSN = os.Getenv("POSTGRES_DSN")
if PGDSN == "" {
return fmt.Errorf("You must specify a non-empty POSTGRES_DSN")
}
FieldseekerSchemaDirectory = os.Getenv("FIELDSEEKER_SCHEMA_DIRECTORY")
if FieldseekerSchemaDirectory == "" {
return fmt.Errorf("You must specify a non-empty FIELDSEEKER_SCHEMA_DIRECTORY")
}
FilesDirectoryPublic = os.Getenv("FILES_DIRECTORY_PUBLIC")
if FilesDirectoryPublic == "" {
return fmt.Errorf("You must specify a non-empty FILES_DIRECTORY_PUBLIC")
FilesDirectory = os.Getenv("FILES_DIRECTORY")
if FilesDirectory == "" {
return fmt.Errorf("You must specify a non-empty FILES_DIRECTORY")
}
FilesDirectoryUser = os.Getenv("FILES_DIRECTORY_USER")
if FilesDirectoryUser == "" {
return fmt.Errorf("You must specify a non-empty FILES_DIRECTORY_USER")
ForwardEmailAPIToken = os.Getenv("FORWARDEMAIL_API_TOKEN")
if ForwardEmailAPIToken == "" {
return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_API_TOKEN")
}
ForwardEmailRMOAddress = os.Getenv("FORWARDEMAIL_RMO_ADDRESS")
if ForwardEmailRMOAddress == "" {
return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_RMO_ADDRESS")
}
ForwardEmailRMOUsername = os.Getenv("FORWARDEMAIL_RMO_USERNAME")
if ForwardEmailRMOUsername == "" {
return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_RMO_USERNAME")
}
ForwardEmailRMOPassword = os.Getenv("FORWARDEMAIL_RMO_PASSWORD")
if ForwardEmailRMOPassword == "" {
return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_RMO_PASSWORD")
}
ForwardEmailNidusAddress = os.Getenv("FORWARDEMAIL_NIDUS_ADDRESS")
if ForwardEmailNidusAddress == "" {
return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_NIDUS_ADDRESS")
}
ForwardEmailNidusUsername = os.Getenv("FORWARDEMAIL_NIDUS_USERNAME")
if ForwardEmailNidusUsername == "" {
return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_NIDUS_USERNAME")
}
ForwardEmailNidusPassword = os.Getenv("FORWARDEMAIL_NIDUS_PASSWORD")
if ForwardEmailNidusPassword == "" {
return fmt.Errorf("You must specify a non-empty FORWARDEMAIL_NIDUS_PASSWORD")
}
LobAPIKey = os.Getenv("LOB_API_KEY")
if LobAPIKey == "" {
return fmt.Errorf("You must specify a non-empty LOB_API_KEY")
}
PGDSN = os.Getenv("POSTGRES_DSN")
if PGDSN == "" {
return fmt.Errorf("You must specify a non-empty POSTGRES_DSN")
}
PhoneNumberReportStr = os.Getenv("PHONE_NUMBER_RMO")
if PhoneNumberReportStr == "" {
return fmt.Errorf("You must specify a non-empty PHONE_NUMBER_RMO")
}
p, err := phonenumbers.Parse(PhoneNumberReportStr, "US")
if err != nil {
return fmt.Errorf("Failed to parse '%s' as a valid phone number: %w", PhoneNumberReportStr, err)
}
PhoneNumberReport = *p
PhoneNumberSupportStr = os.Getenv("PHONE_NUMBER_SUPPORT")
if PhoneNumberSupportStr == "" {
return fmt.Errorf("You must specify a non-empty PHONE_NUMBER_SUPPORT")
}
p, err = phonenumbers.Parse(PhoneNumberSupportStr, "US")
if err != nil {
return fmt.Errorf("Failed to parse '%s' as a valid phone number: %w", PhoneNumberSupportStr, err)
}
PhoneNumberSupport = *p
SentryDSN = os.Getenv("SENTRY_DSN")
if SentryDSN == "" {
return fmt.Errorf("You must specify a non-empty SENTRY_DSN")
}
SentryDSNFrontend = os.Getenv("SENTRY_DSN_FRONTEND")
if SentryDSNFrontend == "" {
return fmt.Errorf("You must specify a non-empty SENTRY_DSN_FRONTEND")
}
StadiaMapsAPIKey = os.Getenv("STADIA_MAPS_API_KEY")
if StadiaMapsAPIKey == "" {
return fmt.Errorf("You must specify a non-empty STADIA_MAPS_API_KEY")
}
TextProvider = os.Getenv("TEXT_PROVIDER")
switch TextProvider {
case "":
return fmt.Errorf("You must specify a non-empty TEXT_PROVIDER")
case "twilio":
case "voipms":
break
default:
return fmt.Errorf("Unrecognized text provider '%s'", TextProvider)
}
TwilioAccountSID = os.Getenv("TWILIO_ACCOUNT_SID")
if TwilioAccountSID == "" {
return fmt.Errorf("You must specify a non-empty TWILIO_ACCOUNT_SID")
}
TwilioAuthToken = os.Getenv("TWILIO_AUTH_TOKEN")
if TwilioAuthToken == "" {
return fmt.Errorf("You must specify a non-empty TWILIO_AUTH_TOKEN")
}
TwilioMessagingServiceSID = os.Getenv("TWILIO_MESSAGING_SERVICE_SID")
if TwilioMessagingServiceSID == "" {
return fmt.Errorf("You must specify a non-empty TWILIO_MESSAGING_SERVICE_SID")
}
TwilioRCSSenderRMO = os.Getenv("TWILIO_RCS_SENDER_RMO")
if TwilioRCSSenderRMO == "" {
return fmt.Errorf("You must specify a non-empty TWILIO_RCS_SENDER_RMO")
}
VoipMSNumber = os.Getenv("VOIPMS_NUMBER")
if VoipMSNumber == "" {
return fmt.Errorf("You must specify a non-empty VOIPMS_NUMBER")
}
VoipMSPassword = os.Getenv("VOIPMS_PASSWORD")
if VoipMSPassword == "" {
return fmt.Errorf("You must specify a non-empty VOIPMS_PASSWORD")
}
VoipMSUsername = os.Getenv("VOIPMS_USERNAME")
if VoipMSPassword == "" {
return fmt.Errorf("You must specify a non-empty VOIPMS_USERNAME")
}
return nil
}
func RedirectURL() string {
return MakeURLSync("/arcgis/oauth/callback")
func ArcGISOauthRedirectURL() string {
return MakeURLNidus("/oauth/arcgis/callback")
}

1
db/bob

@ -1 +0,0 @@
Subproject commit d277a066d6bac5336e49615495ce2c74e736a7fd

View file

@ -1,2 +1,3 @@
#!/run/current-system/sw/bin/bash
PSQL_DSN="postgresql://?host=/var/run/postgresql&sslmode=disable&dbname=nidus-sync" bob/bobgen-psql
PSQL_DSN="postgresql://?host=/var/run/postgresql&sslmode=disable&dbname=nidus-sync" /tmp/bobgen-psql
#PSQL_DSN="postgresql://?host=/var/run/postgresql&sslmode=disable&dbname=nidus-sync" bob/gen/bobgen-psql/bobgen-psql

View file

@ -1,5 +1,16 @@
aliases:
arcgis.user_:
up_plural: "ArcgisUsers"
up_singular: "ArcgisUser"
down_plural: "arcgisusers"
down_singular: "arcgisuser"
organization:
relationships:
publicreport.pool.pool_organization_id_fkey: "PublicreportPool"
fieldseeker.pool.pool_organization_id_fkey: "FieldseekerPool"
user_:
relationships:
fileupload.pool.pool_creator_id_fkey: "FileuploadPool"
up_plural: "Users"
up_singular: "User"
down_plural: "users"
@ -7,10 +18,39 @@ aliases:
no_tests: true
psql:
schemas:
- "comms"
- "fieldseeker"
- "fileupload"
- "lob"
- "public"
- "publicreport"
- "fieldseeker"
- "tile"
shared_schema: "public"
queries:
- ./sql
uuid_pkg: google
plugins_preset: "all"
plugins:
counts:
disabled: true
dbinfo:
destination: "dbinfo"
disabled: false
pkgname: "dbinfo"
enums:
destination: "enums"
disabled: false
pkgname: "enums"
factory:
disabled: true
pkgname: "factory"
destination: "factory"
joins:
disabled: true
loaders:
disabled: false
models:
destination: "models"
disabled: false
pkgname: "models"
where:
disabled: false

View file

@ -7,38 +7,148 @@ import (
"errors"
"fmt"
"io/fs"
"sync"
//"github.com/georgysavva/scany/v2/pgxscan"
//"github.com/jackc/pgx/v5"
"github.com/Gleipnir-Technology/bob"
"github.com/Gleipnir-Technology/jet/postgres"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/scan"
pgxgeom "github.com/twpayne/pgx-geom"
)
var ErrNoRows = pgx.ErrNoRows
//go:embed migrations/*.sql
var embedMigrations embed.FS
type postgres struct {
type pginstance struct {
BobDB bob.DB
PGXPool *pgxpool.Pool
}
var (
PGInstance *postgres
pgOnce sync.Once
PGInstance *pginstance
)
func ExecuteNone(ctx context.Context, stmt postgres.Statement) error {
query, args := stmt.Sql()
_, err := PGInstance.PGXPool.Query(ctx, query, args...)
return err
}
func ExecuteNoneTx(ctx context.Context, txn Ex, stmt postgres.Statement) error {
query, args := stmt.Sql()
r, err := txn.Query(ctx, query, args...)
if err != nil {
return fmt.Errorf("query: %w", err)
}
r.Close()
return nil
}
func ExecuteNoneTxBob(ctx context.Context, txn bob.Tx, stmt postgres.Statement) error {
query, args := stmt.Sql()
r, err := txn.QueryContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("query: %w", err)
}
r.Close()
return nil
}
func ExecuteOne[T any](ctx context.Context, stmt postgres.Statement) (T, error) {
query, args := stmt.Sql()
var result T
row, err := PGInstance.PGXPool.Query(ctx, query, args...)
if err != nil {
return result, fmt.Errorf("execute query: %w", err)
}
var collected *T
collected, err = pgx.CollectOneRow(row, pgx.RowToAddrOfStructByPos[T])
if err != nil || collected == nil {
return result, fmt.Errorf("collect row: %w", err)
}
return *collected, nil
}
func ExecuteOneTx[T any](ctx context.Context, txn Ex, stmt postgres.Statement) (T, error) {
query, args := stmt.Sql()
//result, err := scan.One(ctx, txn, scan.StructMapper[T](), query, args...)
row, err := txn.Query(ctx, query, args...)
var result T
if err != nil {
return result, fmt.Errorf("txn query: %w", err)
}
var collected *T
collected, err = pgx.CollectOneRow(row, pgx.RowToAddrOfStructByPos[T])
if err != nil || collected == nil {
return result, fmt.Errorf("collect row: %w", err)
}
return *collected, nil
}
func ExecuteOneTxBob[T any](ctx context.Context, txn bob.Tx, stmt postgres.Statement) (T, error) {
query, args := stmt.Sql()
return scan.One(ctx, txn, scan.StructMapper[T](), query, args...)
}
func ExecuteMany[T any](ctx context.Context, stmt postgres.Statement) ([]T, error) {
query, args := stmt.Sql()
rows, err := PGInstance.PGXPool.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("execute query: %w", err)
}
collected, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByPos[T])
if err != nil {
return []T{}, fmt.Errorf("collect rows: %w", err)
}
results := make([]T, len(collected))
for i, c := range collected {
if c == nil {
return results, fmt.Errorf("null collected")
}
results[i] = *c
}
return results, nil
}
func ExecuteManyTx[T any](ctx context.Context, txn Ex, stmt postgres.Statement) ([]T, error) {
query, args := stmt.Sql()
rows, err := txn.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("execute query: %w", err)
}
collected, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByPos[T])
if err != nil {
return []T{}, fmt.Errorf("collect rows: %w", err)
}
results := make([]T, len(collected))
for i, c := range collected {
if c == nil {
return results, fmt.Errorf("null collected")
}
results[i] = *c
}
return results, nil
}
func doMigrations(connection_string string) error {
log.Info().Str("dsn", connection_string).Msg("Connecting to database")
log.Debug().Str("dsn", connection_string).Msg("Connecting to database")
db, err := sql.Open("pgx", connection_string)
if err != nil {
return fmt.Errorf("Failed to open database connection: %w", err)
}
defer db.Close()
defer func() {
err := db.Close()
if err != nil {
log.Error().Err(err).Msg("failed to close database connection")
}
}()
row := db.QueryRowContext(context.Background(), "SELECT version()")
var val string
if err := row.Scan(&val); err != nil {
@ -76,7 +186,7 @@ func doMigrations(connection_string string) error {
}
func InitializeDatabase(ctx context.Context, uri string) error {
log.Info().Str("dsn", uri).Msg("Connecting to database")
log.Debug().Str("dsn", uri).Msg("Initializing database")
needs, err := needsMigrations(uri)
if err != nil {
return fmt.Errorf("Failed to determine if migrations are needed: %w", err)
@ -92,18 +202,26 @@ func InitializeDatabase(ctx context.Context, uri string) error {
return fmt.Errorf("Failed to handle migrations: %w", err)
}
} else {
log.Info().Msg("No database migrations necessary")
log.Debug().Msg("No database migrations necessary")
}
pgOnce.Do(func() {
db, e := pgxpool.New(ctx, uri)
bobDB := bob.NewDB(stdlib.OpenDBFromPool(db))
PGInstance = &postgres{bobDB, db}
err = e
})
config, err := pgxpool.ParseConfig(uri)
if err != nil {
return fmt.Errorf("unable to create connection pool: %w", err)
return fmt.Errorf("parse config: %w", err)
}
config.AfterConnect = func(ctx2 context.Context, conn *pgx.Conn) error {
err2 := pgxgeom.Register(ctx, conn)
if err2 != nil {
return fmt.Errorf("pgxgeom register: %w", err2)
}
return nil
}
db, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return fmt.Errorf("new pool: %w", err)
}
bobDB := bob.NewDB(stdlib.OpenDBFromPool(db))
PGInstance = &pginstance{bobDB, db}
var current string
query := `SELECT current_database()`
@ -111,11 +229,6 @@ func InitializeDatabase(ctx context.Context, uri string) error {
if err != nil {
return fmt.Errorf("Failed to get database current: %w", err)
}
log.Info().Str("database", current).Msg("Connected to database")
err = prepareStatements(ctx)
if err != nil {
return fmt.Errorf("Failed to initialize prepared statements: %w", err)
}
return nil
}
@ -124,7 +237,12 @@ func needsMigrations(connection_string string) (*bool, error) {
if err != nil {
return nil, fmt.Errorf("Failed to open database connection: %w", err)
}
defer db.Close()
defer func() {
err := db.Close()
if err != nil {
log.Error().Err(err).Msg("failed to close database connection")
}
}()
row := db.QueryRowContext(context.Background(), "SELECT version()")
var val string
if err := row.Scan(&val); err != nil {

View file

@ -0,0 +1,26 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var AddressErrors = &addressErrors{
ErrUniqueAddressPkey: &UniqueConstraintError{
schema: "",
table: "address",
columns: []string{"id"},
s: "address_pkey",
},
ErrUniqueAddressGidUnique: &UniqueConstraintError{
schema: "",
table: "address",
columns: []string{"gid"},
s: "address_gid_unique",
},
}
type addressErrors struct {
ErrUniqueAddressPkey *UniqueConstraintError
ErrUniqueAddressGidUnique *UniqueConstraintError
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommsEmailContactErrors = &commsEmailContactErrors{
ErrUniqueEmailPkey: &UniqueConstraintError{
schema: "comms",
table: "email_contact",
columns: []string{"address"},
s: "email_pkey",
},
}
type commsEmailContactErrors struct {
ErrUniqueEmailPkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommsEmailLogErrors = &commsEmailLogErrors{
ErrUniqueEmailLogPkey: &UniqueConstraintError{
schema: "comms",
table: "email_log",
columns: []string{"id"},
s: "email_log_pkey",
},
}
type commsEmailLogErrors struct {
ErrUniqueEmailLogPkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommsEmailTemplateErrors = &commsEmailTemplateErrors{
ErrUniqueEmailTemplatePkey: &UniqueConstraintError{
schema: "comms",
table: "email_template",
columns: []string{"id"},
s: "email_template_pkey",
},
}
type commsEmailTemplateErrors struct {
ErrUniqueEmailTemplatePkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommsMailerErrors = &commsMailerErrors{
ErrUniqueMailerPkey: &UniqueConstraintError{
schema: "comms",
table: "mailer",
columns: []string{"id"},
s: "mailer_pkey",
},
}
type commsMailerErrors struct {
ErrUniqueMailerPkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommsPhoneErrors = &commsPhoneErrors{
ErrUniquePhonePkey: &UniqueConstraintError{
schema: "comms",
table: "phone",
columns: []string{"e164"},
s: "phone_pkey",
},
}
type commsPhoneErrors struct {
ErrUniquePhonePkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommsTextJobErrors = &commsTextJobErrors{
ErrUniqueTextJobPkey: &UniqueConstraintError{
schema: "comms",
table: "text_job",
columns: []string{"id"},
s: "text_job_pkey",
},
}
type commsTextJobErrors struct {
ErrUniqueTextJobPkey *UniqueConstraintError
}

View file

@ -0,0 +1,26 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommsTextLogErrors = &commsTextLogErrors{
ErrUniqueTextLogPkey: &UniqueConstraintError{
schema: "comms",
table: "text_log",
columns: []string{"id"},
s: "text_log_pkey",
},
ErrUniqueTextLogTwilioSidKey: &UniqueConstraintError{
schema: "comms",
table: "text_log",
columns: []string{"twilio_sid"},
s: "text_log_twilio_sid_key",
},
}
type commsTextLogErrors struct {
ErrUniqueTextLogPkey *UniqueConstraintError
ErrUniqueTextLogTwilioSidKey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommunicationErrors = &communicationErrors{
ErrUniqueCommunicationPkey: &UniqueConstraintError{
schema: "",
table: "communication",
columns: []string{"id"},
s: "communication_pkey",
},
}
type communicationErrors struct {
ErrUniqueCommunicationPkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var CommunicationLogEntryErrors = &communicationLogEntryErrors{
ErrUniqueCommunicationLogEntryPkey: &UniqueConstraintError{
schema: "",
table: "communication_log_entry",
columns: []string{"id"},
s: "communication_log_entry_pkey",
},
}
type communicationLogEntryErrors struct {
ErrUniqueCommunicationLogEntryPkey *UniqueConstraintError
}

View file

@ -0,0 +1,26 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var ComplianceReportRequestErrors = &complianceReportRequestErrors{
ErrUniqueComplianceReportRequestPkey: &UniqueConstraintError{
schema: "",
table: "compliance_report_request",
columns: []string{"id"},
s: "compliance_report_request_pkey",
},
ErrUniqueComplianceReportRequestPublicIdKey: &UniqueConstraintError{
schema: "",
table: "compliance_report_request",
columns: []string{"public_id"},
s: "compliance_report_request_public_id_key",
},
}
type complianceReportRequestErrors struct {
ErrUniqueComplianceReportRequestPkey *UniqueConstraintError
ErrUniqueComplianceReportRequestPublicIdKey *UniqueConstraintError
}

View file

@ -0,0 +1,26 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var ComplianceReportRequestMailerErrors = &complianceReportRequestMailerErrors{
ErrUniqueComplianceReportRequestMailerPkey: &UniqueConstraintError{
schema: "",
table: "compliance_report_request_mailer",
columns: []string{"id"},
s: "compliance_report_request_mailer_pkey",
},
ErrUniqueComplianceReportRequestMaiComplianceReportRequestId_Key: &UniqueConstraintError{
schema: "",
table: "compliance_report_request_mailer",
columns: []string{"compliance_report_request_id", "mailer_id"},
s: "compliance_report_request_mai_compliance_report_request_id__key",
},
}
type complianceReportRequestMailerErrors struct {
ErrUniqueComplianceReportRequestMailerPkey *UniqueConstraintError
ErrUniqueComplianceReportRequestMaiComplianceReportRequestId_Key *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var DistrictSubscriptionEmailErrors = &districtSubscriptionEmailErrors{
ErrUniqueDistrictSubscriptionEmailPkey: &UniqueConstraintError{
schema: "",
table: "district_subscription_email",
columns: []string{"organization_id", "email_contact_address"},
s: "district_subscription_email_pkey",
},
}
type districtSubscriptionEmailErrors struct {
ErrUniqueDistrictSubscriptionEmailPkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var DistrictSubscriptionPhoneErrors = &districtSubscriptionPhoneErrors{
ErrUniqueDistrictSubscriptionPhonePkey: &UniqueConstraintError{
schema: "",
table: "district_subscription_phone",
columns: []string{"organization_id", "phone_e164"},
s: "district_subscription_phone_pkey",
},
}
type districtSubscriptionPhoneErrors struct {
ErrUniqueDistrictSubscriptionPhonePkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var FeatureErrors = &featureErrors{
ErrUniqueFeaturePkey: &UniqueConstraintError{
schema: "",
table: "feature",
columns: []string{"id"},
s: "feature_pkey",
},
}
type featureErrors struct {
ErrUniqueFeaturePkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var FeaturePoolErrors = &featurePoolErrors{
ErrUniqueFeaturePoolPkey: &UniqueConstraintError{
schema: "",
table: "feature_pool",
columns: []string{"feature_id"},
s: "feature_pool_pkey",
},
}
type featurePoolErrors struct {
ErrUniqueFeaturePoolPkey *UniqueConstraintError
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerContainerrelateErrors = &fieldseekerContainerrelateErrors{
ErrUniqueContainerrelatePkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "containerrelate",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "containerrelate_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerFieldscoutinglogErrors = &fieldseekerFieldscoutinglogErrors{
ErrUniqueFieldscoutinglogPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "fieldscoutinglog",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "fieldscoutinglog_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerHabitatrelateErrors = &fieldseekerHabitatrelateErrors{
ErrUniqueHabitatrelatePkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "habitatrelate",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "habitatrelate_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerInspectionsampleErrors = &fieldseekerInspectionsampleErrors{
ErrUniqueInspectionsamplePkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "inspectionsample",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "inspectionsample_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerInspectionsampledetailErrors = &fieldseekerInspectionsampledetail
ErrUniqueInspectionsampledetailPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "inspectionsampledetail",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "inspectionsampledetail_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerLinelocationErrors = &fieldseekerLinelocationErrors{
ErrUniqueLinelocationPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "linelocation",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "linelocation_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerLocationtrackingErrors = &fieldseekerLocationtrackingErrors{
ErrUniqueLocationtrackingPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "locationtracking",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "locationtracking_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerMosquitoinspectionErrors = &fieldseekerMosquitoinspectionErrors{
ErrUniqueMosquitoinspectionPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "mosquitoinspection",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "mosquitoinspection_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerPointlocationErrors = &fieldseekerPointlocationErrors{
ErrUniquePointlocationPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "pointlocation",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "pointlocation_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerPolygonlocationErrors = &fieldseekerPolygonlocationErrors{
ErrUniquePolygonlocationPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "polygonlocation",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "polygonlocation_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerPoolErrors = &fieldseekerPoolErrors{
ErrUniquePoolPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "pool",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "pool_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerPooldetailErrors = &fieldseekerPooldetailErrors{
ErrUniquePooldetailPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "pooldetail",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "pooldetail_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerProposedtreatmentareaErrors = &fieldseekerProposedtreatmentareaEr
ErrUniqueProposedtreatmentareaPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "proposedtreatmentarea",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "proposedtreatmentarea_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerQamosquitoinspectionErrors = &fieldseekerQamosquitoinspectionErro
ErrUniqueQamosquitoinspectionPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "qamosquitoinspection",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "qamosquitoinspection_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerRodentlocationErrors = &fieldseekerRodentlocationErrors{
ErrUniqueRodentlocationPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "rodentlocation",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "rodentlocation_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerSamplecollectionErrors = &fieldseekerSamplecollectionErrors{
ErrUniqueSamplecollectionPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "samplecollection",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "samplecollection_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerSamplelocationErrors = &fieldseekerSamplelocationErrors{
ErrUniqueSamplelocationPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "samplelocation",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "samplelocation_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerServicerequestErrors = &fieldseekerServicerequestErrors{
ErrUniqueServicerequestPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "servicerequest",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "servicerequest_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerSpeciesabundanceErrors = &fieldseekerSpeciesabundanceErrors{
ErrUniqueSpeciesabundancePkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "speciesabundance",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "speciesabundance_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerStormdrainErrors = &fieldseekerStormdrainErrors{
ErrUniqueStormdrainPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "stormdrain",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "stormdrain_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerTimecardErrors = &fieldseekerTimecardErrors{
ErrUniqueTimecardPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "timecard",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "timecard_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerTrapdatumErrors = &fieldseekerTrapdatumErrors{
ErrUniqueTrapdataPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "trapdata",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "trapdata_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerTraplocationErrors = &fieldseekerTraplocationErrors{
ErrUniqueTraplocationPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "traplocation",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "traplocation_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerTreatmentErrors = &fieldseekerTreatmentErrors{
ErrUniqueTreatmentPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "treatment",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "treatment_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerTreatmentareaErrors = &fieldseekerTreatmentareaErrors{
ErrUniqueTreatmentareaPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "treatmentarea",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "treatmentarea_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerZoneErrors = &fieldseekerZoneErrors{
ErrUniqueZonesPkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "zones",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "zones_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
@ -7,7 +7,7 @@ var FieldseekerZones2Errors = &fieldseekerZones2Errors{
ErrUniqueZones2Pkey: &UniqueConstraintError{
schema: "fieldseeker",
table: "zones2",
columns: []string{"objectid", "version"},
columns: []string{"globalid", "version"},
s: "zones2_pkey",
},
}

View file

@ -1,4 +1,4 @@
// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT.
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var FileuploadCSVErrors = &fileuploadCSVErrors{
ErrUniqueCsvPkey: &UniqueConstraintError{
schema: "fileupload",
table: "csv",
columns: []string{"file_id"},
s: "csv_pkey",
},
}
type fileuploadCSVErrors struct {
ErrUniqueCsvPkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var FileuploadErrorCSVErrors = &fileuploadErrorCSVErrors{
ErrUniqueErrorCsvPkey: &UniqueConstraintError{
schema: "fileupload",
table: "error_csv",
columns: []string{"id"},
s: "error_csv_pkey",
},
}
type fileuploadErrorCSVErrors struct {
ErrUniqueErrorCsvPkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var FileuploadErrorFileErrors = &fileuploadErrorFileErrors{
ErrUniqueErrorFilePkey: &UniqueConstraintError{
schema: "fileupload",
table: "error_file",
columns: []string{"id"},
s: "error_file_pkey",
},
}
type fileuploadErrorFileErrors struct {
ErrUniqueErrorFilePkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var FileuploadFileErrors = &fileuploadFileErrors{
ErrUniqueFilePkey: &UniqueConstraintError{
schema: "fileupload",
table: "file",
columns: []string{"id"},
s: "file_pkey",
},
}
type fileuploadFileErrors struct {
ErrUniqueFilePkey *UniqueConstraintError
}

View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var FileuploadPoolErrors = &fileuploadPoolErrors{
ErrUniquePoolPkey: &UniqueConstraintError{
schema: "fileupload",
table: "pool",
columns: []string{"id"},
s: "pool_pkey",
},
}
type fileuploadPoolErrors struct {
ErrUniquePoolPkey *UniqueConstraintError
}

Some files were not shown because too many files have changed in this diff Show more