Compare commits

...

10 commits

Author SHA1 Message Date
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
21 changed files with 273 additions and 104 deletions

View file

@ -44,14 +44,22 @@ func ExecuteNone(ctx context.Context, stmt postgres.Statement) error {
func ExecuteNoneTx(ctx context.Context, txn Ex, stmt postgres.Statement) error {
query, args := stmt.Sql()
_, err := txn.Query(ctx, query, args...)
return err
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()
_, err := txn.QueryContext(ctx, query, args...)
return err
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()

View file

@ -18,7 +18,7 @@ func AccountFromID(ctx context.Context, org_id string) (model.Account, error) {
return db.ExecuteOne[model.Account](ctx, statement)
}
func AccountInsert(ctx context.Context, txn bob.Tx, m *model.Account) (model.Account, error) {
statement := table.Account.INSERT(table.Account.AllColumns).
statement := table.Account.INSERT(table.Account.MutableColumns).
MODEL(m).
RETURNING(table.Account.AllColumns)
return db.ExecuteOneTxBob[model.Account](ctx, txn, statement)

View file

@ -52,6 +52,9 @@ func AddressFromID(ctx context.Context, txn db.Ex, comm_id int64) (model.Address
return db.ExecuteOne[model.Address](ctx, statement)
}
func AddressesFromIDs(ctx context.Context, txn db.Ex, address_ids []int64) ([]model.Address, error) {
if len(address_ids) == 0 {
return []model.Address{}, nil
}
sql_ids := make([]postgres.Expression, len(address_ids))
for i, address_id := range address_ids {
sql_ids[i] = postgres.Int(address_id)

View file

@ -13,9 +13,16 @@ import (
type ComplianceUpdater = db.Updater[table.ComplianceTable, model.Compliance]
func NewComplianceUpdater() ComplianceUpdater {
return db.NewUpdater[table.ComplianceTable, model.Compliance](
table.Compliance,
table.Compliance.ReportID,
)
}
func NewUpdaterCompliance() db.Updater[table.ComplianceTable, model.Compliance] {
return db.NewUpdater[table.ComplianceTable, model.Compliance](
*table.Compliance,
table.Compliance,
table.Compliance.ReportID,
)

View file

@ -10,7 +10,7 @@ import (
)
func ImageInsert(ctx context.Context, txn db.Ex, m model.Image) (model.Image, error) {
statement := table.Image.INSERT(table.Image.AllColumns).
statement := table.Image.INSERT(table.Image.MutableColumns).
MODEL(m).
RETURNING(table.Image.AllColumns)
return db.ExecuteOneTx[model.Image](ctx, txn, statement)

View file

@ -11,7 +11,7 @@ import (
)
func NuisanceInsert(ctx context.Context, txn db.Ex, m model.Nuisance) (model.Nuisance, error) {
statement := table.Nuisance.INSERT(table.Nuisance.AllColumns).
statement := table.Nuisance.INSERT(table.Nuisance.MutableColumns).
MODEL(m).
RETURNING(table.Nuisance.AllColumns)
return db.ExecuteOneTx[model.Nuisance](ctx, txn, statement)

View file

@ -15,8 +15,15 @@ import (
type ReportUpdater = db.Updater[table.ReportTable, model.Report]
func NewReportUpdater() ReportUpdater {
return db.NewUpdater[table.ReportTable, model.Report](
table.Report,
table.Report.ID,
)
}
func ReportInsert(ctx context.Context, txn db.Ex, m model.Report) (model.Report, error) {
statement := table.Report.INSERT(table.Report.AllColumns).
statement := table.Report.INSERT(table.Report.MutableColumns).
MODEL(m).
RETURNING(table.Report.AllColumns)
return db.ExecuteOneTx[model.Report](ctx, txn, statement)

View file

@ -10,13 +10,13 @@ import (
)
func ReportImageInsert(ctx context.Context, txn db.Ex, m model.ReportImage) (model.ReportImage, error) {
statement := table.ReportImage.INSERT(table.ReportImage.AllColumns).
statement := table.ReportImage.INSERT(table.ReportImage.MutableColumns).
MODEL(m).
RETURNING(table.ReportImage.AllColumns)
return db.ExecuteOneTx[model.ReportImage](ctx, txn, statement)
}
func ReportImagesInsert(ctx context.Context, txn db.Ex, m []model.ReportImage) ([]model.ReportImage, error) {
statement := table.ReportImage.INSERT(table.ReportImage.AllColumns).
statement := table.ReportImage.INSERT(table.ReportImage.MutableColumns).
MODELS(m).
RETURNING(table.ReportImage.AllColumns)
return db.ExecuteManyTx[model.ReportImage](ctx, txn, statement)

View file

@ -10,7 +10,7 @@ import (
)
func ReportLogInsert(ctx context.Context, txn db.Ex, m model.ReportLog) (model.ReportLog, error) {
statement := table.ReportLog.INSERT(table.ReportLog.AllColumns).
statement := table.ReportLog.INSERT(table.ReportLog.MutableColumns).
MODEL(m).
RETURNING(table.ReportLog.AllColumns)
return db.ExecuteOneTx[model.ReportLog](ctx, txn, statement)

View file

@ -11,7 +11,7 @@ import (
)
func WaterInsert(ctx context.Context, txn db.Ex, m model.Water) (model.Water, error) {
statement := table.Water.INSERT(table.Water.AllColumns).
statement := table.Water.INSERT(table.Water.MutableColumns).
MODEL(m).
RETURNING(table.Water.AllColumns)
return db.ExecuteOneTx[model.Water](ctx, txn, statement)

View file

@ -2,6 +2,7 @@ package db
import (
"context"
"fmt"
//"github.com/go-jet/jet/v2"
"github.com/go-jet/jet/v2/postgres"
@ -17,6 +18,13 @@ type Updater[T postgres.Table, M any] struct {
}
func (u Updater[T, M]) Execute(ctx context.Context, txn Ex, pk_values ...interface{}) error {
// We get syntax errors from the database if there are no updates to perform
if u.Columns == nil {
return fmt.Errorf("nil columns")
}
if len(u.Columns) == 0 {
return nil
}
statement := u.Table.
UPDATE(u.Columns).
MODEL(u.Model).
@ -47,12 +55,12 @@ func (u *Updater[T, M]) Unset(c postgres.Column) {
}
}
func NewUpdater[T postgres.Table, M any](
table T,
table *T,
pk_columns ...postgres.ColumnInteger,
) Updater[T, M] {
return Updater[T, M]{
Columns: postgres.ColumnList{},
Table: table,
Table: *table,
buildWhere: func(pk_values ...interface{}) postgres.BoolExpression {
conditions := make([]postgres.BoolExpression, len(pk_columns))
for i, col := range pk_columns {

View file

@ -89,7 +89,7 @@ func PublicReportInvalid(ctx context.Context, user User, public_id string) error
}
now := time.Now()
report_updater := querypublicreport.ReportUpdater{}
report_updater := querypublicreport.NewReportUpdater()
report_updater.Model.Reviewed = &now
report_updater.Set(tablepublicreport.Report.Reviewed)
reporter_id := int32(user.ID)
@ -187,6 +187,12 @@ func PublicReportUpdateCompliance(ctx context.Context, public_id string, report_
}
// Avoid attempting to perform an empty update
if address != nil {
report_updates.Model.AddressGid = address.GID
report_updates.Set(tablepublicreport.Report.AddressGid)
report_updates.Model.AddressRaw = address.Raw
report_updates.Set(tablepublicreport.Report.AddressRaw)
}
err = report_updates.Execute(ctx, txn, int64(report.ID))
if err != nil {
return fmt.Errorf("update report: %w", err)
@ -196,7 +202,7 @@ func PublicReportUpdateCompliance(ctx context.Context, public_id string, report_
return fmt.Errorf("update compliance: %w", err)
}
if address != nil {
err = publicReportUpdateAddress(ctx, txn, report, *address)
err = publicReportUpdateAddressID(ctx, txn, report, *address)
if err != nil {
return fmt.Errorf("update address: %w", err)
}
@ -224,7 +230,7 @@ func PublicReportComplianceCreate(ctx context.Context, setter_report modelpublic
setter_compliance.ReportID = report_id
_, err := querypublicreport.ComplianceInsert(ctx, txn, setter_compliance)
if err != nil {
return fmt.Errorf("Failed to create nuisance database record: %w", err)
return fmt.Errorf("Failed to create compliance database record: %w", err)
}
return nil
})
@ -406,34 +412,35 @@ func publicReportCreate(ctx context.Context, setter_report modelpublicreport.Rep
)
return result, nil
}
func publicReportUpdateAddress(ctx context.Context, txn db.Tx, report *modelpublicreport.Report, address types.Address) error {
statement := tablepublicreport.Report.UPDATE(
tablepublicreport.Report.AddressGid,
tablepublicreport.Report.AddressRaw,
).SET(
postgres.String(address.GID),
postgres.String(address.Raw),
).FROM(tablepublic.Address).
WHERE(
func publicReportUpdateAddressID(ctx context.Context, txn db.Tx, report *modelpublicreport.Report, address types.Address) error {
var err error
if address.GID == "" && address.Raw != "" {
geo_res, err := geocode.GeocodeRaw(ctx, nil, address.Raw)
if err != nil {
return fmt.Errorf("Failed to geocode raw: %w", err)
}
statement := tablepublicreport.Report.UPDATE(
tablepublicreport.Report.AddressID,
).SET(
tablepublicreport.Report.AddressID.SET(postgres.Int(int64(*geo_res.Address.ID))),
).WHERE(
tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))),
)
err := db.ExecuteNoneTx(ctx, txn, statement)
if err != nil {
return fmt.Errorf("update report: %w", err)
}
statement = tablepublicreport.Report.UPDATE(
tablepublicreport.Report.AddressID,
).SET(
tablepublic.Address.SELECT(
tablepublic.Address.ID,
err = db.ExecuteNoneTx(ctx, txn, statement)
} else {
statement := tablepublicreport.Report.UPDATE(
tablepublicreport.Report.AddressID,
).SET(
tablepublic.Address.SELECT(
tablepublic.Address.ID,
).WHERE(
tablepublic.Address.Gid.EQ(postgres.String(address.GID)),
).LIMIT(1),
).WHERE(
tablepublic.Address.Gid.EQ(postgres.String(address.GID)),
).LIMIT(1),
).WHERE(
tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))),
)
err = db.ExecuteNoneTx(ctx, txn, statement)
tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))),
)
err = db.ExecuteNoneTx(ctx, txn, statement)
}
if err != nil {
return fmt.Errorf("update report address_id: %w", err)
}

View file

@ -8,10 +8,10 @@ import (
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/db"
querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public"
querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport"
modelpublic "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model"
modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model"
querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public"
querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
//"github.com/google/uuid"
"github.com/rs/zerolog/log"
@ -80,6 +80,8 @@ func reportQueryToRows(ctx context.Context, reports []modelpublicreport.Report,
report_ids[i] = report.ID
if report.AddressID != nil {
address_ids = append(address_ids, int64(*report.AddressID))
} else {
log.Debug().Int32("id", report.ID).Msg("has no address")
}
}
images_by_id, err := loadImagesForReport(ctx, report_ids)
@ -124,30 +126,34 @@ func reportQueryToRows(ctx context.Context, reports []modelpublicreport.Report,
address = &a
}
if address == nil {
return nil, fmt.Errorf("nil address: %w", err)
address = &types.Address{
ID: row.AddressID,
GID: row.AddressGid,
Raw: row.AddressRaw,
}
}
results[i] = types.PublicReport{
Address: *address,
Concerns: nil,
Created: row.Created,
ID: row.ID,
Images: images,
Location: location,
Log: logs,
Address: *address,
Concerns: nil,
Created: row.Created,
ID: row.ID,
Images: images,
Location: location,
Log: logs,
DistrictID: &row.OrganizationID,
District: nil,
PublicID: row.PublicID,
District: nil,
PublicID: row.PublicID,
Reporter: types.Contact{
CanSMS: &row.ReporterPhoneCanSms,
Email: &row.ReporterEmail,
CanSMS: &row.ReporterPhoneCanSms,
Email: &row.ReporterEmail,
HasEmail: row.ReporterEmail != "",
HasPhone: row.ReporterPhone != "",
Name: &row.ReporterName,
Phone: &row.ReporterPhone,
Name: &row.ReporterName,
Phone: &row.ReporterPhone,
},
Status: row.Status.String(),
Type: row.ReportType.String(),
URI: "",
Type: row.ReportType.String(),
URI: "",
}
}
return results, nil

View file

@ -152,7 +152,7 @@ func SignalCreateFromPublicreport(ctx context.Context, user User, report_id stri
if err != nil {
return nil, fmt.Errorf("create signal: %w", err)
}
report_updater := querypublicreport.ReportUpdater{}
report_updater := querypublicreport.NewReportUpdater()
now := time.Now()
report_updater.Model.Reviewed = &now
report_updater.Set(tablepublicreport.Report.Reviewed)

View file

@ -4,6 +4,7 @@ import (
"context"
"net/http"
"strconv"
"time"
"github.com/Gleipnir-Technology/nidus-sync/config"
modelpublic "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model"
@ -26,12 +27,13 @@ func Communication(r *router) *communicationR {
}
type communicationLog struct {
Created string `json:"string"`
ID string `json:"id"`
Type string `json:"type"`
User string `json:"user"`
Created time.Time `json:"created"`
ID string `json:"id"`
Type string `json:"type"`
User string `json:"user"`
}
type communication struct {
Created time.Time `json:"created"`
ID string `json:"id"`
Log []communicationLog `json:"log"`
Response string `json:"response"`
@ -40,6 +42,14 @@ type communication struct {
Type string `json:"type"`
URI string `json:"uri"`
}
type communicationStub struct {
Created time.Time `json:"created"`
ID string `json:"id"`
Source string `json:"source"`
Status string `json:"status"`
Type string `json:"type"`
URI string `json:"uri"`
}
func toImageURLs(m map[string][]uuid.UUID, id string) []string {
uuids, ok := m[id]
@ -55,7 +65,7 @@ func toImageURLs(m map[string][]uuid.UUID, id string) []string {
func (res *communicationR) Get(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*communication, *nhttp.ErrorWithStatus) {
return nil, nil
}
func (res *communicationR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]communication, *nhttp.ErrorWithStatus) {
func (res *communicationR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]communicationStub, *nhttp.ErrorWithStatus) {
comms, err := platform.CommunicationsForOrganization(ctx, int64(user.Organization.ID))
if err != nil {
return nil, nhttp.NewError("nuisance report query: %w", err)
@ -74,13 +84,13 @@ func (res *communicationR) List(ctx context.Context, r *http.Request, user platf
for _, pr := range public_reports {
public_report_id_to_report[pr.ID] = pr
}
result := make([]communication, len(comms))
result := make([]communicationStub, len(comms))
for i, comm := range comms {
public_report, ok := public_report_id_to_report[*comm.SourceReportID]
if !ok {
return nil, nhttp.NewError("lookup report id %d failed", comm.SourceReportID)
}
c, err := res.hydrateCommunication(comm, &public_report)
c, err := res.hydrateCommunicationStub(comm, &public_report)
if err != nil {
return nil, err
}
@ -105,42 +115,64 @@ func (res *communicationR) MarkPossibleResolved(ctx context.Context, r *http.Req
}
func (res *communicationR) hydrateCommunication(comm modelpublic.Communication, public_report *modelpublicreport.Report) (communication, *nhttp.ErrorWithStatus) {
var err error
source_uri := "unknown"
type_ := "unknown"
if comm.SourceReportID != nil && public_report != nil {
source_uri, err = reportURI(res.router, "", public_report.PublicID)
if err != nil {
return communication{}, nhttp.NewError("gen report URI: %w", err)
}
type_ = "publicreport." + public_report.ReportType.String()
} else if comm.SourceEmailLogID != nil {
source_uri, err = emailURI(*res.router, *comm.SourceEmailLogID)
if err != nil {
return communication{}, nhttp.NewError("gen email URI: %w", err)
}
type_ = "email"
} else if comm.SourceTextLogID != nil {
source_uri, err = textURI(*res.router, *comm.SourceTextLogID)
if err != nil {
return communication{}, nhttp.NewError("gen email URI: %w", err)
}
source_uri = "text"
stub, err := res.hydrateCommunicationStub(comm, public_report)
if err != nil {
return communication{}, nhttp.NewError("hydrate stub: %w", err)
}
response, err := responseURI(*res.router, comm)
if err != nil {
return communication{}, nhttp.NewError("gen response URI: %w", err)
}
return communication{
Created: stub.Created,
ID: stub.ID,
Response: response,
Source: stub.Source,
Status: stub.Status,
Type: stub.Type,
URI: stub.URI,
}, nil
}
func (res *communicationR) hydrateCommunicationStub(comm modelpublic.Communication, public_report *modelpublicreport.Report) (communicationStub, *nhttp.ErrorWithStatus) {
var err error
source_uri := "unknown"
type_ := "unknown"
if comm.SourceReportID != nil && public_report != nil {
source_uri, err = reportURI(res.router, "", public_report.PublicID)
if err != nil {
return communicationStub{}, nhttp.NewError("gen report URI: %w", err)
}
type_ = "publicreport." + public_report.ReportType.String()
} else if comm.SourceEmailLogID != nil {
source_uri, err = emailURI(*res.router, *comm.SourceEmailLogID)
if err != nil {
return communicationStub{}, nhttp.NewError("gen email URI: %w", err)
}
type_ = "email"
} else if comm.SourceTextLogID != nil {
source_uri, err = textURI(*res.router, *comm.SourceTextLogID)
if err != nil {
return communicationStub{}, nhttp.NewError("gen email URI: %w", err)
}
source_uri = "text"
}
/*
response, err := responseURI(*res.router, comm)
if err != nil {
return communicationStub{}, nhttp.NewError("gen response URI: %w", err)
}
*/
uri, err := res.router.IDToURI("communication.ByIDGet", int(comm.ID))
if err != nil {
return communication{}, nhttp.NewError("gen comm uri: %w", err)
return communicationStub{}, nhttp.NewError("gen comm uri: %w", err)
}
return communication{
ID: strconv.Itoa(int(comm.ID)),
Response: response,
Source: source_uri,
Status: comm.Status.String(),
Type: type_,
URI: uri,
return communicationStub{
Created: comm.Created,
ID: strconv.Itoa(int(comm.ID)),
Source: source_uri,
Status: comm.Status.String(),
Type: type_,
URI: uri,
}, nil
}

View file

@ -152,9 +152,9 @@ func (res *complianceR) Update(ctx context.Context, r *http.Request, prf publicR
if public_id == "" {
return nil, nhttp.NewBadRequest("You must provide an ID")
}
report_updater := querypublicreport.ReportUpdater{}
report_updater := querypublicreport.NewReportUpdater()
//report_setter := models.PublicreportReportSetter{}
compliance_updater := querypublicreport.ComplianceUpdater{}
compliance_updater := querypublicreport.NewComplianceUpdater()
//compliance_setter := models.PublicreportComplianceSetter{}
var location *types.Location
if prf.Location.IsValue() {

View file

@ -1,14 +1,50 @@
<style>
.global-error-toast {
position: fixed;
top: 20px;
right: 20px;
background: #c00;
color: white;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 9999;
max-width: 400px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
<template>
<router-view />
<div id="app">
<div v-if="error" class="global-error-toast">
{{ error.message }}
<button @click="errorClear">x</button>
</div>
<router-view />
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { apiClient } from "@/client";
import router from "@/route/config";
import { useErrorHandler } from "@/composable/error-handler";
import { SSEManager, type SSEMessageResource } from "@/SSEManager";
const { error, errorClear } = useErrorHandler();
onMounted(() => {
SSEManager.connect("/api/events");
SSEManager.subscribe((msg: SSEMessageResource) => {

View file

@ -0,0 +1,29 @@
import { ref } from "vue";
interface ErrorState {
hasError: boolean;
message: string;
timestamp: Date;
}
const globalError = ref<ErrorState | null>(null);
export function useErrorHandler() {
const setError = (error: Error) => {
globalError.value = {
hasError: true,
message: error.message,
timestamp: new Date(),
};
};
const errorClear = () => {
globalError.value = null;
};
return {
error: globalError,
setError,
errorClear,
};
}

View file

@ -174,8 +174,13 @@ function updateModel(
emit("update:modelValue", newLocator);
}
onMounted(() => {
const geo_config = {
enableHighAccuracy: true,
maximumAge: Infinity,
timeout: 10000,
};
storeLocation
.get()
.get(geo_config)
.then((loc: GeolocationPosition) => {
console.log("user geolocation", loc);
const coords = loc.coords;

View file

@ -15,11 +15,15 @@ export const useStoreLocation = defineStore("location", () => {
}
// Default options if none provided
const geolocationOptions = options || {
const geolocationOptions =
options ||
{
/*
enableHighAccuracy: true,
timeout: 5000,
timeout: 60000,
maximumAge: 0,
};
*/
};
// Call the geolocation API
navigator.geolocation.getCurrentPosition(

View file

@ -4,6 +4,7 @@ import App from "@/AppSync.vue";
import * as config from "@/config";
import router from "@/route/config";
import * as sentry from "@/sentry";
import { useErrorHandler } from "@/composable/error-handler";
import "maplibre-gl/dist/maplibre-gl.css";
@ -18,8 +19,24 @@ import "@/gen/custom-icons.scss";
import * as bootstrap from "bootstrap";
window.bootstrap = bootstrap;
const { setError } = useErrorHandler();
const pinia = createPinia();
const app = createApp(App);
app.config.errorHandler = (err, instance, info) => {
// err: the error object
// instance: the component instance where error occurred
// info: Vue-specific error info, e.g., lifecycle hook
console.error("Global error:", err);
console.error("Error info:", info);
console.error("Error instance:", instance);
// You could dispatch to a store, send to error tracking service, etc.
// For example, trigger a global error state
setError(err);
};
app.use(pinia);
app.use(router);
sentry.Init(app, pinia).then(() => {