diff --git a/README.md b/README.md new file mode 100644 index 0000000..a95d905 --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ + +# GOptivum + +## Overview + +**GOptivum** is a modern replacement for the legacy schedule generator program from Vulcan, named Optivum. It is designed to scrape schedule data from [zsem.edu.pl/plany](https://zsem.edu.pl/plany) and provide data persistence, an improved (definitely) UI/UX, and a publicly available API with Server-Sent Events (SSE). This project is intended to be used as a replacement for my school's version of the software, but can easily (the scraper package) be modified to work with other school's versions of that software. + +> **Note**: Vulcan *(hawk-tuah)* is a **very evil** and **greedy** corporation and actively blocks open-source projects, making it difficult for developers to create alternatives to their poorly made software. For more information on this situation I suggest you visit this site [czyvulcanapojebalo.pl](https://czyvulcanapojebalo.pl/) + +## Features + +- **Data Scraping**: Scrapes schedule data from [zsem.edu.pl/plany](https://zsem.edu.pl/plany). +- **Data Persistence**: Ensures data availability even if the source site is down. +- **Modern UI/UX**: Provides a way better user interface and experience. +- **Public API**: Offers a publicly available API with SSE for real-time updates. + +## Usage + +You can currently use the application by visiting [zsem.smuggr.xyz](https://zsem.smuggr.xyz). Please note that the site may be down occasionally for maintenance as it is self-hosted. + +## Deployment + +To deploy GOptivum, you need to download a precompiled binary or compile it from source. The following tools are required: + +- Go (version >1.23.1) +- Make +- Node.js +- Vite +- rsync (for deploying to the server) + +> **Important**: Before using the `make deploy` command, edit the credentials in the `Makefile` to match your server setup. + +## Steps to Deploy + +### 1. Clone the Repository + +```bash +git clone https://github.com/smugg99/goptivum.git +cd goptivum +``` + +### 2. Build the application + +```bash +make +``` + +### 3. Deploy to Server + +```bash +make deploy +``` + +## API Endpoints + +The API provides several endpoints for accessing and managing schedule data, weather, and other related resources. + +### Health Check + +- **[GET] - `/api/v1/health/ping`** Returns a basic health check response to confirm the API is running. + +--- + +### Schedule Data + +#### Divisions + +- **[GET] - `/api/v1/divisions/`** Retrieves the list of all divisions. +- **[GET] - `/api/v1/division/{index}`** Retrieves the schedule for a specific division by its index. + +#### Teachers + +- **[GET] - `/api/v1/teachers/`** Retrieves the list of all teachers. +- **[GET] - `/api/v1/teacher/{index}`** Retrieves the schedule for a specific teacher by their index. + +#### Rooms + +- **[GET] - `/api/v1/rooms/`** Retrieves the list of all rooms. +- **[GET] - `/api/v1/room/{index}`** Retrieves the schedule for a specific room by its index. + +--- + +### Events + +Provides real-time updates using **Server-Sent Events (SSE)**. + +- **[GET] - `/api/v1/events/divisions`** Sends updates for divisions. +- **[GET] - `/api/v1/events/teachers`** Sends updates for teachers. +- **[GET] - `/api/v1/events/rooms`** Sends updates for rooms. + +--- + +### Weather + +- **[GET] - `/api/v1/weather/forecast`** Retrieves the weather forecast for the next 3 days. +- **[GET] - `/api/v1/weather/current`** Retrieves the current weather information. + +--- + +### Air Quality + +- **[GET] - `/api/v1/air/current`** Retrieves current air pollution data. + +--- + +> **Note**: +> The API supports the `application/protobuf` response format for efficient data serialization. Clients can specify this format in the `Accept` header of their requests (JSON is the default format). +> Also, replace `{index}` with the specific index of the resource you want to query (e.g., division, teacher, or room). + +## TODO for V1.1.0 + +- [ ] Enhance error handling and logging (including analytics) throughout the application. +- [ ] Optimize the deployment process and documentation. +- [ ] Implement user authentication and authorization for the API. +- [ ] Add more detailed API documentation and examples. +- [ ] Create a Dockerfile for easier deployment. + +## Contributions + +Contributions to the project are very welcome! If you can make the scraper more generic and capable of working with more versions of the Optivum software, it would be highly appreciated. Feel free to fork the repository, submit pull requests, or reach out with your ideas and improvements. + +## Gallery + +### Home + +
+ Home Page Dark + Home Page Light +
+ +### Divisions List + +
+ Divisions Page Dark + Divisions Page Light +
+ +### Division Schedule + +
+ Division Page Dark + Division Page Light +
+ +### Mobile Versions + +
+ Home Page Dark + Mobile Home Page Light + Division Page Dark + Division Page Light + Divisions Page Dark + Divisions Page Light +
+ +## License + +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/api/v1/api.go b/api/v1/api.go index 0c2ff7b..930ac7a 100644 --- a/api/v1/api.go +++ b/api/v1/api.go @@ -6,9 +6,9 @@ import ( "os" "strconv" - "smuggr.xyz/optivum-bsf/api/v1/routes" - "smuggr.xyz/optivum-bsf/common/config" - "smuggr.xyz/optivum-bsf/common/models" + "smuggr.xyz/goptivum/api/v1/routes" + "smuggr.xyz/goptivum/common/config" + "smuggr.xyz/goptivum/common/models" "github.com/gin-contrib/cors" "github.com/gin-contrib/gzip" @@ -44,4 +44,4 @@ func Initialize(scheduleChannels *models.ScheduleChannels) chan error { }() return errCh -} \ No newline at end of file +} diff --git a/api/v1/handlers/atmosphere.go b/api/v1/handlers/atmosphere.go index 2d15f5d..a00e6e9 100644 --- a/api/v1/handlers/atmosphere.go +++ b/api/v1/handlers/atmosphere.go @@ -9,164 +9,164 @@ import ( "sort" "time" - "smuggr.xyz/optivum-bsf/common/models" + "smuggr.xyz/goptivum/common/models" "github.com/gin-gonic/gin" ) func WeatherForecastHandler(c *gin.Context) { - lang := c.DefaultQuery("lang", "en") - units := c.DefaultQuery("units", "metric") - apiKey := os.Getenv("OPENWEATHER_API_KEY") - - url := fmt.Sprintf("%s%s", - Config.OpenWeather.BaseUrl, - fmt.Sprintf(Config.OpenWeather.Endpoints.ForecastWeather, Config.OpenWeather.Lat, Config.OpenWeather.Lon, apiKey, lang, units, 40), - ) - - // #nosec G107 - resp, err := http.Get(url) - if err != nil { - Respond(c, http.StatusInternalServerError, models.APIResponse{ - Message: "failed to fetch forecast data", - Success: false, - }) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - Respond(c, http.StatusInternalServerError, models.APIResponse{ - Message: "failed to fetch forecast data", - Success: false, - }) - return - } - - var openWeatherData map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&openWeatherData); err != nil { - Respond(c, http.StatusInternalServerError, models.APIResponse{ - Message: "failed to parse forecast data", - Success: false, - }) - return - } - - forecastList, ok := openWeatherData["list"].([]interface{}) - if !ok { - Respond(c, http.StatusInternalServerError, models.APIResponse{ - Message: "invalid forecast data format", - Success: false, - }) - return - } - - forecastResponse := &models.ForecastResponse{ - Name: openWeatherData["city"].(map[string]interface{})["name"].(string), - } - - forecastsByDate := make(map[string][]map[string]interface{}) - - for _, forecast := range forecastList { - forecastMap := forecast.(map[string]interface{}) - dt := int64(forecastMap["dt"].(float64)) - t := time.Unix(dt, 0).UTC() - dateStr := t.Format("2006-01-02") - - forecastsByDate[dateStr] = append(forecastsByDate[dateStr], forecastMap) - } - - var dates []string - for dateStr := range forecastsByDate { - dates = append(dates, dateStr) - } - sort.Strings(dates) - - now := time.Now().UTC() - - datesProcessed := 0 - for _, dateStr := range dates { - forecastDate, err := time.Parse("2006-01-02", dateStr) - if err != nil { - continue - } - - if forecastDate.Before(now.Truncate(24 * time.Hour)) { - continue - } - - forecasts := forecastsByDate[dateStr] - - var closestForecast map[string]interface{} - minTimeDiff := int64(1<<63 - 1) - - for _, fMap := range forecasts { - fDt := int64(fMap["dt"].(float64)) - fTime := time.Unix(fDt, 0).UTC() - - if fTime.Before(now) { - continue - } - - timeDiff := fDt - now.Unix() - - if timeDiff < minTimeDiff { - minTimeDiff = timeDiff - closestForecast = fMap - } - } - - if closestForecast != nil { - conditionList, ok := closestForecast["weather"].([]interface{}) - if !ok || len(conditionList) == 0 { - continue - } - conditionMap, ok := conditionList[0].(map[string]interface{}) - if !ok { - continue - } - - temperatureMap, ok := closestForecast["main"].(map[string]interface{}) - if !ok { - continue - } - - cityMap, ok := openWeatherData["city"].(map[string]interface{}) - if !ok { - continue - } - - sunrise := int64(cityMap["sunrise"].(float64)) - sunset := int64(cityMap["sunset"].(float64)) - - dt := int64(closestForecast["dt"].(float64)) - t := time.Unix(dt, 0).UTC() - dayOfWeek := int64(t.Weekday()) - - forecastResponse.Forecast = append(forecastResponse.Forecast, &models.Forecast{ - Condition: &models.Condition{ - Name: conditionMap["main"].(string), - Description: conditionMap["description"].(string), - }, - Temperature: &models.Temperature{ - Current: temperatureMap["temp"].(float64), - Min: temperatureMap["temp_min"].(float64), - Max: temperatureMap["temp_max"].(float64), - }, - Sunrise: sunrise, - Sunset: sunset, - DayOfWeek: dayOfWeek, - }) - - datesProcessed++ - - if datesProcessed >= 3 { - break - } - } - } - - Respond(c, http.StatusOK, forecastResponse) + lang := c.DefaultQuery("lang", "en") + units := c.DefaultQuery("units", "metric") + apiKey := os.Getenv("OPENWEATHER_API_KEY") + + url := fmt.Sprintf("%s%s", + Config.OpenWeather.BaseUrl, + fmt.Sprintf(Config.OpenWeather.Endpoints.ForecastWeather, Config.OpenWeather.Lat, Config.OpenWeather.Lon, apiKey, lang, units, 40), + ) + + // #nosec G107 + resp, err := http.Get(url) + if err != nil { + Respond(c, http.StatusInternalServerError, models.APIResponse{ + Message: "failed to fetch forecast data", + Success: false, + }) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + Respond(c, http.StatusInternalServerError, models.APIResponse{ + Message: "failed to fetch forecast data", + Success: false, + }) + return + } + + var openWeatherData map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&openWeatherData); err != nil { + Respond(c, http.StatusInternalServerError, models.APIResponse{ + Message: "failed to parse forecast data", + Success: false, + }) + return + } + + forecastList, ok := openWeatherData["list"].([]interface{}) + if !ok { + Respond(c, http.StatusInternalServerError, models.APIResponse{ + Message: "invalid forecast data format", + Success: false, + }) + return + } + + forecastResponse := &models.ForecastResponse{ + Name: openWeatherData["city"].(map[string]interface{})["name"].(string), + } + + forecastsByDate := make(map[string][]map[string]interface{}) + + for _, forecast := range forecastList { + forecastMap := forecast.(map[string]interface{}) + dt := int64(forecastMap["dt"].(float64)) + t := time.Unix(dt, 0).UTC() + dateStr := t.Format("2006-01-02") + + forecastsByDate[dateStr] = append(forecastsByDate[dateStr], forecastMap) + } + + var dates []string + for dateStr := range forecastsByDate { + dates = append(dates, dateStr) + } + sort.Strings(dates) + + now := time.Now().UTC() + + datesProcessed := 0 + for _, dateStr := range dates { + forecastDate, err := time.Parse("2006-01-02", dateStr) + if err != nil { + continue + } + + if forecastDate.Before(now.Truncate(24 * time.Hour)) { + continue + } + + forecasts := forecastsByDate[dateStr] + + var closestForecast map[string]interface{} + minTimeDiff := int64(1<<63 - 1) + + for _, fMap := range forecasts { + fDt := int64(fMap["dt"].(float64)) + fTime := time.Unix(fDt, 0).UTC() + + if fTime.Before(now) { + continue + } + + timeDiff := fDt - now.Unix() + + if timeDiff < minTimeDiff { + minTimeDiff = timeDiff + closestForecast = fMap + } + } + + if closestForecast != nil { + conditionList, ok := closestForecast["weather"].([]interface{}) + if !ok || len(conditionList) == 0 { + continue + } + conditionMap, ok := conditionList[0].(map[string]interface{}) + if !ok { + continue + } + + temperatureMap, ok := closestForecast["main"].(map[string]interface{}) + if !ok { + continue + } + + cityMap, ok := openWeatherData["city"].(map[string]interface{}) + if !ok { + continue + } + + sunrise := int64(cityMap["sunrise"].(float64)) + sunset := int64(cityMap["sunset"].(float64)) + + dt := int64(closestForecast["dt"].(float64)) + t := time.Unix(dt, 0).UTC() + dayOfWeek := int64(t.Weekday()) + + forecastResponse.Forecast = append(forecastResponse.Forecast, &models.Forecast{ + Condition: &models.Condition{ + Name: conditionMap["main"].(string), + Description: conditionMap["description"].(string), + }, + Temperature: &models.Temperature{ + Current: temperatureMap["temp"].(float64), + Min: temperatureMap["temp_min"].(float64), + Max: temperatureMap["temp_max"].(float64), + }, + Sunrise: sunrise, + Sunset: sunset, + DayOfWeek: dayOfWeek, + }) + + datesProcessed++ + + if datesProcessed >= 3 { + break + } + } + } + + Respond(c, http.StatusOK, forecastResponse) } func CurrentWeatherHandler(c *gin.Context) { diff --git a/api/v1/handlers/handlers.go b/api/v1/handlers/handlers.go index 97e098d..3fa96df 100644 --- a/api/v1/handlers/handlers.go +++ b/api/v1/handlers/handlers.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - "smuggr.xyz/optivum-bsf/common/config" - "smuggr.xyz/optivum-bsf/common/models" + "smuggr.xyz/goptivum/common/config" + "smuggr.xyz/goptivum/common/models" "github.com/gin-gonic/gin" "google.golang.org/protobuf/proto" @@ -16,33 +16,33 @@ import ( var Config *config.APIConfig func Respond(c *gin.Context, code int, data interface{}) { - accept := c.GetHeader("Accept") - switch { - case strings.Contains(accept, "application/protobuf"): - protoMsg, ok := data.(proto.Message) - if !ok { - c.ProtoBuf(http.StatusInternalServerError, models.APIResponse{ - Message: "internal server error", - Success: false, - }) - return - } - c.ProtoBuf(code, protoMsg) - case strings.Contains(accept, "application/json"): - fallthrough - default: - c.JSON(code, data) - } + accept := c.GetHeader("Accept") + switch { + case strings.Contains(accept, "application/protobuf"): + protoMsg, ok := data.(proto.Message) + if !ok { + c.ProtoBuf(http.StatusInternalServerError, models.APIResponse{ + Message: "internal server error", + Success: false, + }) + return + } + c.ProtoBuf(code, protoMsg) + case strings.Contains(accept, "application/json"): + fallthrough + default: + c.JSON(code, data) + } } func PingHandler(c *gin.Context) { Respond(c, http.StatusOK, models.APIResponse{ - Message: "pong", - Success: true, - }) + Message: "pong", + Success: true, + }) } func Initialize() { fmt.Println("initializing handlers") Config = &config.Global.API -} \ No newline at end of file +} diff --git a/api/v1/handlers/schedule.go b/api/v1/handlers/schedule.go index 37cc63c..3916dc7 100644 --- a/api/v1/handlers/schedule.go +++ b/api/v1/handlers/schedule.go @@ -5,9 +5,9 @@ import ( "net/http" "strconv" - "smuggr.xyz/optivum-bsf/common/models" - "smuggr.xyz/optivum-bsf/core/datastore" - "smuggr.xyz/optivum-bsf/core/scraper" + "smuggr.xyz/goptivum/common/models" + "smuggr.xyz/goptivum/core/datastore" + "smuggr.xyz/goptivum/core/scraper" "github.com/gin-gonic/gin" ) @@ -52,7 +52,7 @@ func GetTeacherHandler(c *gin.Context) { if err != nil { Respond(c, http.StatusBadRequest, models.APIResponse{ Message: "invalid index", - Success: false, + Success: false, }) return } @@ -62,7 +62,7 @@ func GetTeacherHandler(c *gin.Context) { if teacher == nil { Respond(c, http.StatusNotFound, models.APIResponse{ Message: "teacher not found", - Success: false, + Success: false, }) return } @@ -86,7 +86,7 @@ func GetRoomHandler(c *gin.Context) { if err != nil { Respond(c, http.StatusBadRequest, models.APIResponse{ Message: "invalid index", - Success: false, + Success: false, }) return } diff --git a/api/v1/routes/atmosphere.go b/api/v1/routes/atmosphere.go index 58e5635..24c77c3 100644 --- a/api/v1/routes/atmosphere.go +++ b/api/v1/routes/atmosphere.go @@ -2,8 +2,8 @@ package routes import ( - "smuggr.xyz/optivum-bsf/api/v1/handlers" "github.com/gin-gonic/gin" + "smuggr.xyz/goptivum/api/v1/handlers" ) func SetupWeatherRoutes(router *gin.Engine, rootGroup *gin.RouterGroup) { diff --git a/api/v1/routes/generic.go b/api/v1/routes/generic.go index 13f2b03..7edcd5b 100644 --- a/api/v1/routes/generic.go +++ b/api/v1/routes/generic.go @@ -4,9 +4,9 @@ package routes import ( "fmt" - "smuggr.xyz/optivum-bsf/api/v1/handlers" - "smuggr.xyz/optivum-bsf/common/models" - "smuggr.xyz/optivum-bsf/core/sse" + "smuggr.xyz/goptivum/api/v1/handlers" + "smuggr.xyz/goptivum/common/models" + "smuggr.xyz/goptivum/core/sse" "github.com/gin-gonic/gin" ) @@ -21,7 +21,7 @@ func SetupGenericRoutes(router *gin.Engine, rootGroup *gin.RouterGroup, schedule var TeachersHub = sse.NewHub(Config.MaxSSEClients) var RoomsHub = sse.NewHub(Config.MaxSSEClients) - go DivisionsHub.Run() + go DivisionsHub.Run() go TeachersHub.Run() go RoomsHub.Run() @@ -30,7 +30,7 @@ func SetupGenericRoutes(router *gin.Engine, rootGroup *gin.RouterGroup, schedule sseGroup.GET("/divisions", func(c *gin.Context) { DivisionsHub.Handler()(c.Writer, c.Request) }) - + sseGroup.GET("/teachers", func(c *gin.Context) { TeachersHub.Handler()(c.Writer, c.Request) }) @@ -60,4 +60,4 @@ func SetupGenericRoutes(router *gin.Engine, rootGroup *gin.RouterGroup, schedule RoomsHub.Broadcast(message) } }() -} \ No newline at end of file +} diff --git a/api/v1/routes/routes.go b/api/v1/routes/routes.go index 60cb221..bf70de9 100644 --- a/api/v1/routes/routes.go +++ b/api/v1/routes/routes.go @@ -4,9 +4,9 @@ package routes import ( "os" - "smuggr.xyz/optivum-bsf/common/config" - "smuggr.xyz/optivum-bsf/common/models" - "smuggr.xyz/optivum-bsf/api/v1/handlers" + "smuggr.xyz/goptivum/api/v1/handlers" + "smuggr.xyz/goptivum/common/config" + "smuggr.xyz/goptivum/common/models" //"github.com/didip/tollbooth" //"github.com/didip/tollbooth_gin" @@ -18,7 +18,7 @@ var Config config.APIConfig func Initialize(defaultRouter *gin.Engine, scheduleChannels *models.ScheduleChannels) { Config = config.Global.API - + //defaultLimiter := tollbooth.NewLimiter(0.5, nil) defaultRouter.Use(static.Serve("/", static.LocalFile(os.Getenv("DIST_PATH"), false))) diff --git a/api/v1/routes/schedule.go b/api/v1/routes/schedule.go index c255ffb..f65fa2c 100644 --- a/api/v1/routes/schedule.go +++ b/api/v1/routes/schedule.go @@ -2,7 +2,7 @@ package routes import ( - "smuggr.xyz/optivum-bsf/api/v1/handlers" + "smuggr.xyz/goptivum/api/v1/handlers" "github.com/gin-gonic/gin" ) @@ -32,6 +32,6 @@ func SetupScheduleRoutes(router *gin.Engine, rootGroup *gin.RouterGroup) { } roomsGroup := rootGroup.Group("/rooms") { - roomsGroup.GET("/", handlers.GetRoomsHandler) + roomsGroup.GET("/", handlers.GetRoomsHandler) } } diff --git a/app/main.go b/app/main.go index ca9d87e..fa0e3e1 100644 --- a/app/main.go +++ b/app/main.go @@ -7,11 +7,11 @@ import ( "os/signal" "syscall" - "smuggr.xyz/optivum-bsf/api/v1" - "smuggr.xyz/optivum-bsf/common/config" - "smuggr.xyz/optivum-bsf/common/models" - "smuggr.xyz/optivum-bsf/core/datastore" - "smuggr.xyz/optivum-bsf/core/scraper" + v1 "smuggr.xyz/goptivum/api/v1" + "smuggr.xyz/goptivum/common/config" + "smuggr.xyz/goptivum/common/models" + "smuggr.xyz/goptivum/core/datastore" + "smuggr.xyz/goptivum/core/scraper" ) func WaitForTermination() { @@ -27,7 +27,7 @@ func Cleanup() { fmt.Println("cleaning up...") scraper.Cleanup() - datastore.Cleanup(); + datastore.Cleanup() } func main() { @@ -49,8 +49,8 @@ func main() { Teachers: scraper.TeachersScraperResource.RefreshChan, Rooms: scraper.RoomsScraperResource.RefreshChan, }) - + defer Cleanup() WaitForTermination() -} \ No newline at end of file +} diff --git a/core/datastore/crud.go b/core/datastore/crud.go index a0b40fa..b6bf341 100644 --- a/core/datastore/crud.go +++ b/core/datastore/crud.go @@ -4,120 +4,120 @@ package datastore import ( "fmt" - "smuggr.xyz/optivum-bsf/common/models" + "smuggr.xyz/goptivum/common/models" "github.com/dgraph-io/badger/v3" "google.golang.org/protobuf/proto" ) func setItem(key []byte, item proto.Message) error { - data, err := proto.Marshal(item) - if err != nil { - return err - } - - return DB.Update(func(txn *badger.Txn) error { - return txn.Set(key, data) - }) + data, err := proto.Marshal(item) + if err != nil { + return err + } + + return DB.Update(func(txn *badger.Txn) error { + return txn.Set(key, data) + }) } func getItem(key []byte, item proto.Message) error { - return DB.View(func(txn *badger.Txn) error { - entry, err := txn.Get(key) - if err != nil { - return err - } - - val, err := entry.ValueCopy(nil) - if err != nil { - return err - } - - return proto.Unmarshal(val, item) - }) + return DB.View(func(txn *badger.Txn) error { + entry, err := txn.Get(key) + if err != nil { + return err + } + + val, err := entry.ValueCopy(nil) + if err != nil { + return err + } + + return proto.Unmarshal(val, item) + }) } func deleteItem(key []byte) error { - return DB.Update(func(txn *badger.Txn) error { - err := txn.Delete(key) - if err != nil && err != badger.ErrKeyNotFound { - return err - } - return nil - }) + return DB.Update(func(txn *badger.Txn) error { + err := txn.Delete(key) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + return nil + }) } func SetDivision(division *models.Division) error { - key := []byte(fmt.Sprintf("division:%d", division.Index)) - return setItem(key, division) + key := []byte(fmt.Sprintf("division:%d", division.Index)) + return setItem(key, division) } func GetDivision(index int64) (*models.Division, error) { - key := []byte(fmt.Sprintf("division:%d", index)) - division := &models.Division{} - err := getItem(key, division) - if err != nil { - return nil, err - } - return division, nil + key := []byte(fmt.Sprintf("division:%d", index)) + division := &models.Division{} + err := getItem(key, division) + if err != nil { + return nil, err + } + return division, nil } func DeleteDivision(index int64) error { - key := []byte(fmt.Sprintf("division:%d", index)) - return deleteItem(key) + key := []byte(fmt.Sprintf("division:%d", index)) + return deleteItem(key) } func SetTeacher(teacher *models.Teacher) error { - key := []byte(fmt.Sprintf("teacher:%d", teacher.Index)) - return setItem(key, teacher) + key := []byte(fmt.Sprintf("teacher:%d", teacher.Index)) + return setItem(key, teacher) } func GetTeacher(index int64) (*models.Teacher, error) { - key := []byte(fmt.Sprintf("teacher:%d", index)) - teacher := &models.Teacher{} - err := getItem(key, teacher) - if err != nil { - return nil, err - } - return teacher, nil + key := []byte(fmt.Sprintf("teacher:%d", index)) + teacher := &models.Teacher{} + err := getItem(key, teacher) + if err != nil { + return nil, err + } + return teacher, nil } func DeleteTeacher(index int64) error { - key := []byte(fmt.Sprintf("teacher:%d", index)) - return deleteItem(key) + key := []byte(fmt.Sprintf("teacher:%d", index)) + return deleteItem(key) } func SetRoom(room *models.Room) error { - key := []byte(fmt.Sprintf("room:%d", room.Index)) - return setItem(key, room) + key := []byte(fmt.Sprintf("room:%d", room.Index)) + return setItem(key, room) } func GetRoom(index int64) (*models.Room, error) { - key := []byte(fmt.Sprintf("room:%d", index)) - room := &models.Room{} - err := getItem(key, room) - if err != nil { - return nil, err - } - return room, nil + key := []byte(fmt.Sprintf("room:%d", index)) + room := &models.Room{} + err := getItem(key, room) + if err != nil { + return nil, err + } + return room, nil } func DeleteRoom(index int64) error { - key := []byte(fmt.Sprintf("room:%d", index)) - return deleteItem(key) + key := []byte(fmt.Sprintf("room:%d", index)) + return deleteItem(key) } func SetMetadata(metadata *models.Metadata) error { - key := []byte("metadata") - return setItem(key, metadata) + key := []byte("metadata") + return setItem(key, metadata) } func GetMetadata() (*models.Metadata, error) { - key := []byte("metadata") - metadata := &models.Metadata{} - err := getItem(key, metadata) - if err != nil { - return nil, err - } - return metadata, nil -} \ No newline at end of file + key := []byte("metadata") + metadata := &models.Metadata{} + err := getItem(key, metadata) + if err != nil { + return nil, err + } + return metadata, nil +} diff --git a/core/hub/hub.go b/core/hub/hub.go index 09a5c3b..62771ac 100644 --- a/core/hub/hub.go +++ b/core/hub/hub.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "smuggr.xyz/optivum-bsf/core/observer" + "smuggr.xyz/goptivum/core/observer" ) type Hub struct { @@ -18,7 +18,7 @@ type Hub struct { tasksCh chan *observer.Observer workerCount int64 wg sync.WaitGroup - mu sync.RWMutex + mu sync.RWMutex quitCh chan struct{} client *http.Client } @@ -73,25 +73,24 @@ func (h *Hub) RemoveObserver(index int64) { } func (h *Hub) GetObserver(index int64) *observer.Observer { - h.mu.RLock() - defer h.mu.RUnlock() - return h.observers[index] + h.mu.RLock() + defer h.mu.RUnlock() + return h.observers[index] } func (h *Hub) GetAllObservers(ignoreFirst bool) map[int64]*observer.Observer { - h.mu.RLock() - defer h.mu.RUnlock() - copyMap := make(map[int64]*observer.Observer, len(h.observers)) + h.mu.RLock() + defer h.mu.RUnlock() + copyMap := make(map[int64]*observer.Observer, len(h.observers)) if ignoreFirst { delete(copyMap, 0) } - for k, v := range h.observers { - copyMap[k] = v - } - return copyMap + for k, v := range h.observers { + copyMap[k] = v + } + return copyMap } - func (h *Hub) worker(id int64) { defer h.wg.Done() fmt.Printf("worker %d started\n", id) @@ -100,7 +99,7 @@ func (h *Hub) worker(id int64) { select { case o := <-h.tasksCh: fmt.Printf("worker %d: processing observer with URL: %s\n", id, o.URL) - ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) // It's safe to defer cancel here because it's called before the next iteration // However, to avoid multiple defers in a loop, I'll call cancel manually @@ -114,7 +113,7 @@ func (h *Hub) worker(id int64) { fmt.Printf("worker %d: no callback for observer with URL: %s\n", id, o.URL) } } - + case <-h.quitCh: fmt.Printf("stopping worker of id %d\n", id) return @@ -123,64 +122,64 @@ func (h *Hub) worker(id int64) { } func (h *Hub) scheduler() { - defer h.wg.Done() - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case o := <-h.addCh: - h.mu.Lock() - if _, exists := h.observers[o.Index]; exists { - fmt.Printf("observer of index %d already exists\n", o.Index) - } else { + defer h.wg.Done() + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case o := <-h.addCh: + h.mu.Lock() + if _, exists := h.observers[o.Index]; exists { + fmt.Printf("observer of index %d already exists\n", o.Index) + } else { o.Mu.Lock() - o.Hash = "" + o.Hash = "" o.Mu.Unlock() - h.observers[o.Index] = o - fmt.Printf("added observer of index: %d\n", o.Index) - } - h.mu.Unlock() - - case _index := <-h.removeCh: - index, ok := _index.(int64) - if !ok { - fmt.Printf("invalid type for index: %T\n", _index) - continue - } - - h.mu.Lock() - if o, exists := h.observers[index]; exists { + h.observers[o.Index] = o + fmt.Printf("added observer of index: %d\n", o.Index) + } + h.mu.Unlock() + + case _index := <-h.removeCh: + index, ok := _index.(int64) + if !ok { + fmt.Printf("invalid type for index: %T\n", _index) + continue + } + + h.mu.Lock() + if o, exists := h.observers[index]; exists { o.Mu.Lock() - delete(h.observers, index) - fmt.Printf("removed observer of index: %d\n", index) - o.Hash = "" + delete(h.observers, index) + fmt.Printf("removed observer of index: %d\n", index) + o.Hash = "" o.Mu.Unlock() - } else { - fmt.Printf("observer of index %d does not exist\n", index) - } - h.mu.Unlock() - - case <-ticker.C: - now := time.Now() - h.mu.RLock() - for _, o := range h.observers { + } else { + fmt.Printf("observer of index %d does not exist\n", index) + } + h.mu.Unlock() + + case <-ticker.C: + now := time.Now() + h.mu.RLock() + for _, o := range h.observers { o.Mu.Lock() - if o.NextRun.Before(now) || o.NextRun.Equal(now) { - o.NextRun = now.Add(o.Interval) + if o.NextRun.Before(now) || o.NextRun.Equal(now) { + o.NextRun = now.Add(o.Interval) select { case h.tasksCh <- o: case <-time.After(10 * time.Second): fmt.Printf("timed out sending observer with URL: %s to tasksCh\n", o.URL) } - } + } o.Mu.Unlock() - } - h.mu.RUnlock() - - case <-h.quitCh: - fmt.Println("stopping scheduler") - return - } - } -} \ No newline at end of file + } + h.mu.RUnlock() + + case <-h.quitCh: + fmt.Println("stopping scheduler") + return + } + } +} diff --git a/core/scraper/helpers.go b/core/scraper/helpers.go index 03b4e13..f5b0fdd 100644 --- a/core/scraper/helpers.go +++ b/core/scraper/helpers.go @@ -7,10 +7,10 @@ import ( "sync" "time" - "smuggr.xyz/optivum-bsf/common/models" - "smuggr.xyz/optivum-bsf/common/utils" - "smuggr.xyz/optivum-bsf/core/datastore" - "smuggr.xyz/optivum-bsf/core/observer" + "smuggr.xyz/goptivum/common/models" + "smuggr.xyz/goptivum/common/utils" + "smuggr.xyz/goptivum/core/datastore" + "smuggr.xyz/goptivum/core/observer" "github.com/PuerkitoBio/goquery" ) @@ -37,11 +37,11 @@ func waitForFirstRefresh() { totalObservers := divisionObservers + teacherObservers + roomObservers if totalObservers > 0 { - wg.Add(totalObservers) - } else { - fmt.Println("no observers to wait for") - return - } + wg.Add(totalObservers) + } else { + fmt.Println("no observers to wait for") + return + } waitForRefresh := func(ch <-chan int64, count int) { fmt.Println("waiting for refresh:", count) @@ -148,49 +148,49 @@ func parseTimeRange(s string) (models.TimeRange, error) { } func parseLesson(rowElement *goquery.Selection, timeRange *models.TimeRange) ([]*models.Lesson, error) { - var lessons []*models.Lesson - - html, err := rowElement.Html() - if err != nil { - return nil, fmt.Errorf("error getting HTML content: %v", err) - } - - html = strings.ReplaceAll(html, "
", "
") - segments := strings.Split(html, "
") - - for _, segment := range segments { - segment = strings.TrimSpace(segment) - if segment == "" || segment == " " { - continue - } - - segmentHTML := "
" + segment + "
" - - segmentDoc, err := goquery.NewDocumentFromReader(strings.NewReader(segmentHTML)) - if err != nil { - fmt.Println("error parsing segment:", err) - continue - } - - lessonName := strings.TrimSpace(segmentDoc.Find("span.p").First().Text()) - if lessonName == "" { - lessonName = strings.TrimSpace(segmentDoc.Text()) - } - teacher := strings.TrimSpace(segmentDoc.Find("a.n").First().Text()) - division := strings.TrimSpace(segmentDoc.Find("a.o").First().Text()) - room := strings.TrimSpace(segmentDoc.Find("a.s").First().Text()) - - lesson := &models.Lesson{ - TimeRange: timeRange, - FullName: lessonName, - TeacherDesignator: teacher, - DivisionDesignator: division, - RoomDesignator: room, - } - lessons = append(lessons, lesson) - } - - return lessons, nil + var lessons []*models.Lesson + + html, err := rowElement.Html() + if err != nil { + return nil, fmt.Errorf("error getting HTML content: %v", err) + } + + html = strings.ReplaceAll(html, "
", "
") + segments := strings.Split(html, "
") + + for _, segment := range segments { + segment = strings.TrimSpace(segment) + if segment == "" || segment == " " { + continue + } + + segmentHTML := "
" + segment + "
" + + segmentDoc, err := goquery.NewDocumentFromReader(strings.NewReader(segmentHTML)) + if err != nil { + fmt.Println("error parsing segment:", err) + continue + } + + lessonName := strings.TrimSpace(segmentDoc.Find("span.p").First().Text()) + if lessonName == "" { + lessonName = strings.TrimSpace(segmentDoc.Text()) + } + teacher := strings.TrimSpace(segmentDoc.Find("a.n").First().Text()) + division := strings.TrimSpace(segmentDoc.Find("a.o").First().Text()) + room := strings.TrimSpace(segmentDoc.Find("a.s").First().Text()) + + lesson := &models.Lesson{ + TimeRange: timeRange, + FullName: lessonName, + TeacherDesignator: teacher, + DivisionDesignator: division, + RoomDesignator: room, + } + lessons = append(lessons, lesson) + } + + return lessons, nil } func scrapeTitle(doc *goquery.Document) (string, error) { titleSelection := doc.Find("span.tytulnapis").First() @@ -364,8 +364,8 @@ func newDivisionObserver(index int64, refreshChan *chan int64) *observer.Observe } } - url := fmt.Sprintf(Config.BaseUrl + Config.Endpoints.Division, index) - interval := time.Duration((index + 1) / 10 + 5) * time.Second + url := fmt.Sprintf(Config.BaseUrl+Config.Endpoints.Division, index) + interval := time.Duration((index+1)/10+5) * time.Second return observer.NewObserver(index, url, interval, extractFunc, callbackFunc) } @@ -401,8 +401,8 @@ func newTeacherObserver(index int64, refreshChan *chan int64) *observer.Observer } } - url := fmt.Sprintf(Config.BaseUrl + Config.Endpoints.Teacher, index) - interval := time.Duration((index + 1) / 10 + 5) * time.Second + url := fmt.Sprintf(Config.BaseUrl+Config.Endpoints.Teacher, index) + interval := time.Duration((index+1)/10+5) * time.Second return observer.NewObserver(index, url, interval, extractFunc, callbackFunc) } @@ -410,7 +410,7 @@ func newTeacherObserver(index int64, refreshChan *chan int64) *observer.Observer func newRoomObserver(index int64, refreshChan *chan int64) *observer.Observer { extractFunc := func(doc *goquery.Document) string { var content []string - + doc.Find("a").Each(func(i int, s *goquery.Selection) { href, exists := s.Attr("href") if exists { @@ -440,8 +440,8 @@ func newRoomObserver(index int64, refreshChan *chan int64) *observer.Observer { } } - url := fmt.Sprintf(Config.BaseUrl + Config.Endpoints.Room, index) - interval := time.Duration((index + 1) / 10 + 5) * time.Second + url := fmt.Sprintf(Config.BaseUrl+Config.Endpoints.Room, index) + interval := time.Duration((index+1)/10+5) * time.Second return observer.NewObserver(index, url, interval, extractFunc, callbackFunc) -} \ No newline at end of file +} diff --git a/core/scraper/observers.go b/core/scraper/observers.go index d90302d..50dfdfe 100644 --- a/core/scraper/observers.go +++ b/core/scraper/observers.go @@ -6,12 +6,12 @@ import ( "strings" "time" - "smuggr.xyz/optivum-bsf/core/observer" + "smuggr.xyz/goptivum/core/observer" "github.com/PuerkitoBio/goquery" ) -func ObserveDivisions(refreshChan *chan int64) { +func ObserveDivisions(refreshChan *chan int64) { fmt.Println("observing divisions") refreshDivisionsObservers := func() { @@ -117,4 +117,4 @@ func ObserveRooms(refreshChan *chan int64) { RoomsScraperResource.Observer = observer.NewObserver(0, Config.BaseUrl+Config.Endpoints.RoomsList, 1*time.Second, extractFunc, callbackFunc) RoomsScraperResource.Hub.AddObserver(RoomsScraperResource.Observer) refreshRoomsObservers() -} \ No newline at end of file +} diff --git a/core/scraper/scraper.go b/core/scraper/scraper.go index 3d3b4ea..d101e12 100644 --- a/core/scraper/scraper.go +++ b/core/scraper/scraper.go @@ -7,12 +7,12 @@ import ( "strconv" "sync" - "smuggr.xyz/optivum-bsf/common/config" - "smuggr.xyz/optivum-bsf/common/models" - "smuggr.xyz/optivum-bsf/common/utils" - "smuggr.xyz/optivum-bsf/core/datastore" - "smuggr.xyz/optivum-bsf/core/hub" - "smuggr.xyz/optivum-bsf/core/observer" + "smuggr.xyz/goptivum/common/config" + "smuggr.xyz/goptivum/common/models" + "smuggr.xyz/goptivum/common/utils" + "smuggr.xyz/goptivum/core/datastore" + "smuggr.xyz/goptivum/core/hub" + "smuggr.xyz/goptivum/core/observer" "github.com/PuerkitoBio/goquery" ) @@ -22,6 +22,7 @@ import ( var Config config.ScraperConfig type ResourceType string + const ( DivisionResource ResourceType = "division" TeacherResource ResourceType = "teacher" @@ -33,6 +34,7 @@ func (t ResourceType) String() string { } type MetadataType string + const ( DesignatorMetadata MetadataType = "designator" FullNameMetadata MetadataType = "fullname" @@ -51,8 +53,8 @@ type ScraperResource struct { func NewScraperResource(indexRegex *regexp.Regexp, resourceType ResourceType) *ScraperResource { return &ScraperResource{ - Indexes: []int64{}, - Metadata: &models.Metadata{ + Indexes: []int64{}, + Metadata: &models.Metadata{ Designators: make(map[string]*models.Duplicates), FullNames: make(map[string]*models.Duplicates), }, @@ -102,7 +104,7 @@ func (s *ScraperResource) IsIndexInMetadata(index int64, metadataType MetadataTy func (s *ScraperResource) GetDesignatorFromIndex(index int64) string { s.Mu.RLock() defer s.Mu.RUnlock() - + if inDuplicates, designator := s.IsIndexInMetadata(index, DesignatorMetadata); inDuplicates { return designator } @@ -113,7 +115,7 @@ func (s *ScraperResource) GetDesignatorFromIndex(index int64) string { func (s *ScraperResource) GetFullNameFromIndex(index int64) string { s.Mu.RLock() defer s.Mu.RUnlock() - + if inDuplicates, fullName := s.IsIndexInMetadata(index, FullNameMetadata); inDuplicates { return fullName } @@ -390,7 +392,7 @@ func ScrapeDivisionsIndexes() ([]int64, error) { return } endpoint := makeDivisionEndpoint(num) - + if !utils.CheckURL(Config.BaseUrl + endpoint) { fmt.Printf("error opening division " + Config.BaseUrl + endpoint + "\n") return @@ -476,8 +478,8 @@ func Initialize() error { Config = config.Global.Scraper DivisionsScraperResource = NewScraperResource(DivisionIndexRegex, DivisionResource) - TeachersScraperResource = NewScraperResource(TeacherIndexRegex, TeacherResource) - RoomsScraperResource = NewScraperResource(RoomIndexRegex, RoomResource) + TeachersScraperResource = NewScraperResource(TeacherIndexRegex, TeacherResource) + RoomsScraperResource = NewScraperResource(RoomIndexRegex, RoomResource) divisionsIndexes, err := ScrapeDivisionsIndexes() if err != nil { @@ -502,7 +504,7 @@ func Initialize() error { roomsIndexesLength := int64(len(roomsIndexes)) fmt.Printf("starting with %d divisions, %d teachers, %d rooms\n", divisionsIndexesLength, teachersIndexesLength, roomsIndexesLength) - + if divisionsIndexesLength == 0 { fmt.Printf("no divisions found despite %d workers\n", Config.Quantities.Workers.Division) } diff --git a/go.mod b/go.mod index 4d20c19..df23083 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,11 @@ -module smuggr.xyz/optivum-bsf +module smuggr.xyz/goptivum go 1.23.1 require ( github.com/PuerkitoBio/goquery v1.10.0 github.com/dgraph-io/badger/v3 v3.2103.5 - github.com/didip/tollbooth v4.0.2+incompatible - github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505 + github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/gzip v1.0.1 github.com/gin-contrib/static v1.1.2 github.com/gin-gonic/gin v1.9.1 @@ -27,7 +26,6 @@ require ( github.com/dustin/go-humanize v1.0.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/cors v1.7.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -50,7 +48,6 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -60,7 +57,6 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tmaxmax/go-sse v0.8.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.opencensus.io v0.24.0 // indirect @@ -72,7 +68,6 @@ require ( golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect - golang.org/x/time v0.5.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 454e080..6934e65 100644 --- a/go.sum +++ b/go.sum @@ -37,10 +37,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= -github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= -github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505 h1:VkJBA707rG0mOUM5nuqTs53hlJEb6peXnY7elFDWh88= -github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505/go.mod h1:ieayd+rxBVaj62fhAdF5p1U70Y4ZCcfpk0+4jesd0f8= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -152,8 +148,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= @@ -205,8 +199,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tmaxmax/go-sse v0.8.0 h1:pPpTgyyi1r7vG2o6icebnpGEh3ebcnBXqDWkb7aTofs= -github.com/tmaxmax/go-sse v0.8.0/go.mod h1:HLoxqxdH+7oSUItjtnpxjzJedfr/+Rrm/dNWBcTxJFM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -294,8 +286,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/static/basement_floor.jpg b/static/basement_floor.jpg new file mode 100644 index 0000000..b048cbb Binary files /dev/null and b/static/basement_floor.jpg differ diff --git a/static/division_dark.png b/static/division_dark.png new file mode 100644 index 0000000..3988c19 Binary files /dev/null and b/static/division_dark.png differ diff --git a/static/division_light.png b/static/division_light.png new file mode 100644 index 0000000..d8c579a Binary files /dev/null and b/static/division_light.png differ diff --git a/static/division_mobile_dark.png b/static/division_mobile_dark.png new file mode 100644 index 0000000..2de1f66 Binary files /dev/null and b/static/division_mobile_dark.png differ diff --git a/static/division_mobile_dark.png~ b/static/division_mobile_dark.png~ new file mode 100644 index 0000000..6c4429a Binary files /dev/null and b/static/division_mobile_dark.png~ differ diff --git a/static/division_mobile_light.png b/static/division_mobile_light.png new file mode 100644 index 0000000..903fee2 Binary files /dev/null and b/static/division_mobile_light.png differ diff --git a/static/division_mobile_light.png~ b/static/division_mobile_light.png~ new file mode 100644 index 0000000..6246c8f Binary files /dev/null and b/static/division_mobile_light.png~ differ diff --git a/static/divisions_dark.png b/static/divisions_dark.png new file mode 100644 index 0000000..442d467 Binary files /dev/null and b/static/divisions_dark.png differ diff --git a/static/divisions_light.png b/static/divisions_light.png new file mode 100644 index 0000000..2df8e52 Binary files /dev/null and b/static/divisions_light.png differ diff --git a/static/divisions_mobile_dark.png b/static/divisions_mobile_dark.png new file mode 100644 index 0000000..97d9d65 Binary files /dev/null and b/static/divisions_mobile_dark.png differ diff --git a/static/divisions_mobile_light.png b/static/divisions_mobile_light.png new file mode 100644 index 0000000..ee29b75 Binary files /dev/null and b/static/divisions_mobile_light.png differ diff --git a/static/first_floor.jpg b/static/first_floor.jpg new file mode 100644 index 0000000..3660efe Binary files /dev/null and b/static/first_floor.jpg differ diff --git a/static/ground_floor.jpg b/static/ground_floor.jpg new file mode 100644 index 0000000..6772c7f Binary files /dev/null and b/static/ground_floor.jpg differ diff --git a/static/home_dark.png b/static/home_dark.png new file mode 100644 index 0000000..141974d Binary files /dev/null and b/static/home_dark.png differ diff --git a/static/home_light.png b/static/home_light.png new file mode 100644 index 0000000..11335e0 Binary files /dev/null and b/static/home_light.png differ diff --git a/static/home_mobile_dark.png b/static/home_mobile_dark.png new file mode 100644 index 0000000..26670f6 Binary files /dev/null and b/static/home_mobile_dark.png differ diff --git a/static/home_mobile_light.png b/static/home_mobile_light.png new file mode 100644 index 0000000..2415c2b Binary files /dev/null and b/static/home_mobile_light.png differ diff --git a/static/second_floor.jpg b/static/second_floor.jpg new file mode 100644 index 0000000..96d352d Binary files /dev/null and b/static/second_floor.jpg differ diff --git a/web/prod/package-lock.json b/web/prod/package-lock.json index 0740039..c1e4d05 100644 --- a/web/prod/package-lock.json +++ b/web/prod/package-lock.json @@ -1,12 +1,12 @@ { - "name": "optivum-better-schedule-frontend", - "version": "0.0.0", + "name": "goptivum", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "optivum-better-schedule-frontend", - "version": "0.0.0", + "name": "goptivum", + "version": "1.0.3", "dependencies": { "@mdi/font": "7.4.47", "axios": "^1.7.7", diff --git a/web/prod/package.json b/web/prod/package.json index 145ef58..5ab98ff 100644 --- a/web/prod/package.json +++ b/web/prod/package.json @@ -1,6 +1,6 @@ { - "name": "optivum-better-schedule-frontend", - "version": "0.0.0", + "name": "goptivum", + "version": "1.0.3", "scripts": { "build": "vue-tsc --noEmit && vite build", "dev": "vite",