├── Dockerfile ├── LICENSE ├── README.md ├── app.py ├── docker-compose.yml ├── icon.png ├── requirements.txt ├── setup.sh ├── static ├── css │ └── style.css ├── js │ └── script.js ├── links.yaml └── uploads │ └── background.jpg └── templates └── index.html /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use official Python runtime as base image 2 | FROM python:3.9-slim 3 | 4 | # Set working directory in container 5 | WORKDIR /app 6 | 7 | # Create data directory structure 8 | RUN mkdir -p /app/data/static /app/initial_static 9 | 10 | # Copy application files 11 | COPY app.py . 12 | COPY requirements.txt . 13 | COPY templates templates/ 14 | COPY icon.png . 15 | COPY LICENSE . 16 | COPY README.md . 17 | COPY setup.sh . 18 | 19 | # Copy static files to initial_static (as a backup) 20 | COPY static/ /app/initial_static/ 21 | 22 | # Make setup script executable 23 | RUN chmod +x setup.sh 24 | 25 | # Install Flask 26 | RUN pip install flask 27 | 28 | # Make port 5000 available 29 | EXPOSE 5000 30 | 31 | # Define environment variables 32 | ENV FLASK_APP=app.py 33 | ENV FLASK_ENV=development 34 | ENV STATIC_FOLDER=/app/data/static 35 | 36 | # Create volume mount point 37 | VOLUME /app/data 38 | 39 | # Run setup script when container launches 40 | CMD ["./setup.sh"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Joshua White 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleDash 2 | 3 | ![image](https://github.com/user-attachments/assets/01dc0a0d-b23a-49fd-9c88-ca213f6ae000) 4 | 5 | 6 | ## Inspired by FLAME Dashboard but significantly simplified 7 | 8 | A minimalist web-based dashboard featuring categorized links, local applications, and Google Calendar integration with a clean, dark theme interface. 9 | 10 | ## Features 11 | 12 | - 🌅 Dynamic time-based welcome messages 13 | - 🔗 Organized link categories 14 | - 🐳 Docker service shortcuts 15 | - 📅 Google Calendar integration 16 | - 🌙 Dark theme with transparent elements 17 | - 📱 Responsive design 18 | - ⚙️ YAML-based configuration 19 | 20 | ## Setup 21 | 22 | ### Prerequisites 23 | 24 | - Python 3.x (for local server) 25 | - Modern web browser 26 | - Google Calendar (optional) 27 | 28 | ### Quick Start 29 | 30 | 1. Clone or download this repository 31 | 2. Add your background image: 32 | ```bash 33 | mkdir uploads 34 | # Add your image to uploads/ 35 | # Default path: uploads/20240616_084929.jpg 36 | ``` 37 | 3. Start local server: 38 | ```bash 39 | python -m http.server 8000 40 | ``` 41 | 4. Visit `http://localhost:8000` in your browser 42 | 43 | 44 | ### Running with docker 45 | 46 | 1.) Build the docker image 47 | ``` sudo docker build -t simpledash . ``` 48 | 49 | 2.) Run the container 50 | ``` sudo docker run -d -p 5000:5000 simpledash ``` 51 | obviously, you'll need to customize the host port, add a directory if you want to edit thing, etc... 52 | 53 | 54 | ### Configuration 55 | 56 | #### 1. Background Image 57 | 58 | Update image path in `style.css`: 59 | 60 | body:before { 61 | background-image: url("./uploads/your-image.jpg"); 62 | } 63 | 64 | #### 2. Links and Categories (`links.yaml`) 65 | 66 | #### 3. Calendar Integration 67 | 68 | 1. Get your Google Calendar embed URL from Calendar Settings 69 | 2. Update `links.yaml`: 70 | name: "Calendar 1" 71 | url: "your-calendar-embed-url" 72 | 73 | 74 | ## Customization 75 | 76 | ### Theme Colors 77 | 78 | In `style.css`: 79 | - Main container: `background: rgba(0, 0, 0, 0.9)` 80 | - Link items: `background: rgba(0, 0, 0, 0.3)` 81 | - Hover effects: `background: rgba(255, 255, 255, 0.1)` 82 | - Category titles: `color: #228B22` (green) 83 | 84 | ### Icons 85 | 86 | Using [Material Design Icons](https://pictogrammers.com/library/mdi/). Reference icons without the `mdi-` prefix in `links.yaml`. 87 | 88 | ## Structure 89 | ``` 90 | . 91 | ├── app.py # Flask application 92 | ├── Dockerfile # Container configuration 93 | ├── requirements.txt # Python dependencies 94 | ├── setup.sh # Container initialization script 95 | ├── templates/ # HTML templates 96 | │ └── index.html 97 | └── static/ # Static files (copied to volume) 98 | ├── css/ 99 | │ └── style.css 100 | ├── js/ 101 | │ └── script.js 102 | ├── uploads/ 103 | │ └── background.jpg 104 | └── links.yaml 105 | ``` 106 | 107 | ## Browser Support 108 | 109 | - Chrome (recommended) 110 | - Firefox 111 | - Safari 112 | - Edge 113 | 114 | ## Troubleshooting 115 | 116 | ### Common Issues 117 | 118 | 1. **White background/No background image** 119 | - Check image path in `style.css` 120 | - Verify image exists in uploads folder 121 | 122 | 2. **Calendar not loading** 123 | - Verify calendar embed URL 124 | - Check Content Security Policy in `index.html` 125 | 126 | 3. **Icons not showing** 127 | - Check icon names in `links.yaml` (remove mdi- prefix) 128 | - Verify internet connection for CDN 129 | 130 | ## License 131 | 132 | MIT License 133 | 134 | ## Contributing 135 | 136 | Feel free to submit issues and pull requests for improvements! 137 | 138 | ### Docker Deployment 139 | 140 | #### Option 1: Pull from Docker Hub 141 | ```bash 142 | # Pull the image 143 | docker pull securemindorg/simpledash:latest 144 | 145 | # Run the container 146 | # For Windows: 147 | docker run -d -p 5000:5000 -v "%APPDATA%/SimpleDash:/app/data" securemindorg/simpledash:latest 148 | 149 | # For Linux/Mac: 150 | docker run -d -p 5000:5000 -v "$HOME/.simpledash:/app/data" securemindorg/simpledash:latest 151 | ``` 152 | 153 | #### Option 2: Build Locally 154 | 1. Build the Docker image: 155 | ```bash 156 | docker build -t simpledash . 157 | ``` 158 | 159 | 2. Run the container: 160 | ```bash 161 | # For Windows: 162 | docker run -d -p 5000:5000 -v "%APPDATA%/SimpleDash:/app/data" simpledash 163 | 164 | # For Linux/Mac: 165 | docker run -d -p 5000:5000 -v "$HOME/.simpledash:/app/data" simpledash 166 | ``` 167 | ## Security 168 | 169 | Please note this is not intended to be a secure app, don't make it publically accessable 170 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, send_from_directory 2 | import os 3 | 4 | # Get static folder from environment variable or use default 5 | STATIC_FOLDER = os.environ.get('STATIC_FOLDER', 'static') 6 | 7 | # Add debug print 8 | print(f"Using static folder: {STATIC_FOLDER}") 9 | print(f"Static folder exists: {os.path.exists(STATIC_FOLDER)}") 10 | if os.path.exists(STATIC_FOLDER): 11 | print(f"Contents of static folder: {os.listdir(STATIC_FOLDER)}") 12 | if os.path.exists(os.path.join(STATIC_FOLDER, 'js')): 13 | print(f"Contents of js folder: {os.listdir(os.path.join(STATIC_FOLDER, 'js'))}") 14 | if os.path.exists(os.path.join(STATIC_FOLDER, 'css')): 15 | print(f"Contents of css folder: {os.listdir(os.path.join(STATIC_FOLDER, 'css'))}") 16 | 17 | app = Flask(__name__, static_folder=STATIC_FOLDER) 18 | 19 | @app.route('/') 20 | def index(): 21 | print("Serving index.html") 22 | return render_template('index.html') 23 | 24 | @app.route('/static/') 25 | def custom_static(filename): 26 | print(f"Attempting to serve static file: {filename}") 27 | print(f"Looking in directory: {STATIC_FOLDER}") 28 | return send_from_directory(STATIC_FOLDER, filename) 29 | 30 | @app.route('/links.yaml') 31 | def serve_yaml(): 32 | return send_from_directory(STATIC_FOLDER, 'links.yaml') 33 | 34 | if __name__ == '__main__': 35 | app.run(host='0.0.0.0', port=5000, debug=True) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: simpledash 2 | services: 3 | main_app: 4 | cpu_shares: 10 5 | command: [] 6 | deploy: 7 | resources: 8 | limits: 9 | memory: 256M 10 | image: securemindorg/simpledash:latest 11 | labels: 12 | icon: https://github.com/securemindorg/SimpleDash/blob/main/icon.png?raw=true 13 | ports: 14 | - target: 5000 15 | published: "5000" 16 | protocol: tcp 17 | restart: unless-stopped 18 | volumes: 19 | - type: bind 20 | source: /DATA/AppData/SimpleDash 21 | target: /app/data/static 22 | devices: [] 23 | cap_add: [] 24 | environment: [] 25 | network_mode: bridge 26 | privileged: false 27 | container_name: "" 28 | hostname: "" 29 | x-casaos: 30 | author: self 31 | category: self 32 | hostname: "" 33 | icon: https://github.com/securemindorg/SimpleDash/blob/main/icon.png?raw=true 34 | index: / 35 | is_uncontrolled: false 36 | port_map: "5001" 37 | scheme: http 38 | store_app_id: quirky_zakir 39 | title: 40 | custom: SimpleDash 41 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/securemindorg/SimpleDash/7abebbd1337da9e8130b8f927029e2c5e9fbb596/icon.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==3.0.2 -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Check if the static directory is empty 3 | if [ ! -f "/app/data/static/js/script.js" ]; then 4 | echo "Initializing static files..." 5 | cp -r /app/initial_static/* /app/data/static/ 6 | fi 7 | 8 | # Start Flask 9 | flask run --host=0.0.0.0 -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=Roboto:wght@400;500&display=swap'); 2 | 3 | /* Background image */ 4 | body { 5 | margin: 0; 6 | padding: 2rem; 7 | min-height: 100vh; 8 | display: flex; 9 | flex-direction: column; 10 | box-sizing: border-box; 11 | } 12 | 13 | body:before { 14 | content: ""; 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | bottom: 0; 20 | background-image: url("../uploads/background.jpg"); 21 | background-size: cover; 22 | background-position: center; 23 | background-repeat: no-repeat; 24 | width: 100vw; 25 | height: 100vh; 26 | z-index: -999; 27 | filter: blur(0.0rem); 28 | opacity: 0.8; 29 | } 30 | 31 | /* Widen Container Layout */ 32 | .Layout_Container__2Hv3J { 33 | padding: 1.25rem 0 !important; 34 | } 35 | 36 | /* Sets Overall Grid Handling */ 37 | .AppGrid_AppGrid__33iLW { 38 | /* Three Column Layout */ 39 | grid-template-columns: repeat(3, minmax(18.75rem, 18.75rem)) !important; 40 | /* Full Width Layout */ 41 | /*grid-template-columns: repeat(auto-fit, minmax(18.75rem, 18.75rem)) !important; */ 42 | align-items: left !important; 43 | column-gap: 3.125rem !important; 44 | row-gap: 1.2rem !important; 45 | } 46 | 47 | /* AppCard Customisation */ 48 | .AppCard_AppCard__1V2_0 { 49 | padding: 0.8rem !important; 50 | border-radius: 0.6rem !important; 51 | background-color: rgba(0, 0, 0, 0.3) !important; 52 | } 53 | 54 | /* Application tile hover color */ 55 | @supports (-webkit-backdrop-filter: none) or (backdrop-filter: none) { 56 | .AppCard_AppCard__1V2_0:hover { 57 | backdrop-filter: blur(15px); 58 | background-color: rgba(0, 0, 0, 0.6) !important; 59 | } 60 | } 61 | 62 | /* Push description to reasonable space */ 63 | .AppCard_AppCardDetails__tbAhY h5 { 64 | font-size: 0.7692rem !important; 65 | padding-bottom: 0.625rem !important; 66 | } 67 | 68 | /* Settings button style */ 69 | .Home_SettingsButton__Qvn8C { 70 | border-radius: 0.5rem !important; 71 | } 72 | 73 | /* Links Grid Layout */ 74 | .links-grid { 75 | display: flex; 76 | flex-direction: column; 77 | gap: 2rem; 78 | width: calc(50% - 1rem); 79 | } 80 | 81 | .column { 82 | min-width: 0; 83 | width: 100%; 84 | } 85 | 86 | .category-section { 87 | margin-bottom: 2rem; 88 | width: 100%; 89 | } 90 | 91 | .category-title { 92 | color: #228B22; 93 | margin-bottom: 1rem; 94 | background: rgba(3, 3, 3, 0.452); 95 | font-size: 1.1rem; 96 | border-bottom: 1px solid rgba(34, 139, 34, 0.3); 97 | padding-bottom: 0.5rem; 98 | font-weight: 600; 99 | font-family: 'Inter', sans-serif; 100 | letter-spacing: 0.02em; 101 | text-transform: uppercase; 102 | } 103 | 104 | .link-item { 105 | display: flex; 106 | align-items: flex-start; 107 | padding: 0.5rem; 108 | margin-bottom: 0.5rem; 109 | background: rgba(0, 0, 0, 0.5); 110 | border-radius: 0.4rem; 111 | text-decoration: none; 112 | color: #fff; 113 | transition: background-color 0.3s; 114 | width: 100%; 115 | } 116 | 117 | .link-item:hover { 118 | background: rgba(0, 0, 0, 0.7); 119 | } 120 | 121 | .link-item .material-icons { 122 | margin-right: 0.5rem; 123 | } 124 | 125 | /* Responsive design */ 126 | @media (max-width: 768px) { 127 | .links-grid { 128 | grid-template-columns: 1fr; 129 | } 130 | } 131 | 132 | /* Add media query for responsive design */ 133 | @media (max-width: 1200px) { 134 | .links-grid { 135 | width: 100%; 136 | grid-template-columns: 1fr 1fr; 137 | } 138 | } 139 | 140 | main { 141 | display: flex; 142 | gap: 2rem; 143 | padding: 2rem; 144 | } 145 | 146 | .links-grid { 147 | display: flex; 148 | flex-direction: column; 149 | gap: 2rem; 150 | width: calc(50%); 151 | } 152 | 153 | .calendar-frame { 154 | width: calc(50%); 155 | display: flex; 156 | flex-direction: column; 157 | align-items: center; 158 | } 159 | 160 | .calendar-content { 161 | width: 80%; 162 | padding: 1rem; 163 | border-radius: 0.6rem; 164 | min-height: 600px; 165 | } 166 | 167 | .calendar-tabs { 168 | width: 80%; 169 | margin-bottom: 1rem; 170 | padding: 0.5rem; 171 | border-radius: 0.6rem; 172 | } 173 | 174 | .calendar-iframe { 175 | width: 100% !important; 176 | height: 80vh !important; 177 | border-radius: 0.4rem; 178 | } 179 | 180 | @media (max-width: 1200px) { 181 | main { 182 | flex-direction: column; 183 | } 184 | 185 | .links-grid, 186 | .calendar-frame { 187 | width: 100%; 188 | } 189 | } 190 | 191 | .column:first-child .category-section:first-child { 192 | width: 100% !important; 193 | display: flex !important; 194 | flex-direction: column !important; 195 | align-items: flex-start !important; 196 | } 197 | 198 | .column:first-child .category-section:first-child .link-item { 199 | width: 100% !important; 200 | display: flex !important; 201 | justify-content: flex-start !important; 202 | align-items: center !important; 203 | margin-right: auto !important; 204 | } 205 | 206 | .links-wrapper { 207 | width: 100% !important; 208 | display: flex !important; 209 | flex-direction: column !important; 210 | align-items: flex-start !important; 211 | } 212 | 213 | /* Add specific debugging styles */ 214 | .category-section { 215 | border: 1px solid transparent; /* Debug border */ 216 | width: 100% !important; 217 | display: flex !important; 218 | flex-direction: column !important; 219 | } 220 | 221 | .icon-wrapper { 222 | min-width: 24px; 223 | margin-right: 0.5rem; 224 | padding-top: 0.2rem; 225 | } 226 | 227 | .icon-wrapper i { 228 | font-size: 20px; 229 | } 230 | 231 | .link-name { 232 | font-size: 0.95rem; 233 | font-weight: 500; 234 | font-family: 'Roboto', sans-serif; 235 | } 236 | 237 | .link-description { 238 | font-size: 0.75rem; 239 | opacity: 0.8; 240 | color: #e0e0e0; 241 | font-family: 'Roboto', sans-serif; 242 | font-weight: 400; 243 | } 244 | 245 | .content-wrapper { 246 | display: flex; 247 | align-items: flex-start; 248 | width: 100%; 249 | } 250 | 251 | .text-wrapper { 252 | display: flex; 253 | flex-direction: column; 254 | gap: 0.2rem; 255 | } 256 | 257 | #calendar-container iframe { 258 | height: 100vh; 259 | width: 80%; 260 | } 261 | 262 | .calendar-tabs { 263 | display: flex; 264 | gap: 0.5rem; 265 | margin-bottom: 1rem; 266 | } 267 | 268 | .calendar-tab { 269 | padding: 0.5rem 1rem; 270 | background: rgba(0, 0, 0, 0.5); 271 | border: none; 272 | border-radius: 0.4rem; 273 | color: #fff; 274 | cursor: pointer; 275 | font-family: 'Inter', sans-serif; 276 | font-size: 0.9rem; 277 | transition: background-color 0.3s; 278 | } 279 | 280 | .calendar-tab:hover { 281 | background: rgba(0, 0, 0, 0.7); 282 | } 283 | 284 | .calendar-tab.active { 285 | background: rgba(34, 139, 34, 0.5); 286 | } 287 | 288 | .calendar-iframe { 289 | border-radius: 0.4rem; 290 | } 291 | 292 | .upper-section { 293 | width: 100%; 294 | padding: 1rem; 295 | border-radius: 0.6rem; 296 | } 297 | 298 | .docker-grid { 299 | display: grid; 300 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 301 | gap: 1rem; 302 | } 303 | 304 | .docker-service { 305 | background: rgba(0, 0, 0, 0.5); 306 | } 307 | 308 | .docker-service:hover { 309 | background: rgba(0, 0, 0, 0.7); 310 | } 311 | 312 | .lower-section { 313 | width: 100%; 314 | display: grid; 315 | grid-template-columns: 1fr 1fr 1fr; 316 | gap: 2rem; 317 | padding: 1rem; 318 | background: rgba(0, 0, 0, 0.5); 319 | border-radius: 0.6rem; 320 | } 321 | 322 | .column { 323 | min-width: 0; 324 | width: 100%; 325 | display: flex; 326 | flex-direction: column; 327 | gap: 2rem; 328 | } 329 | 330 | @media (max-width: 768px) { 331 | .docker-grid { 332 | grid-template-columns: 1fr; 333 | } 334 | } 335 | 336 | .docker-title { 337 | color: #228B22; 338 | margin-bottom: 1rem; 339 | font-size: 1.1rem; 340 | border-bottom: 1px solid rgba(34, 139, 34, 0.3); 341 | padding-bottom: 0.5rem; 342 | font-weight: 600; 343 | font-family: 'Inter', sans-serif; 344 | letter-spacing: 0.02em; 345 | text-transform: uppercase; 346 | } 347 | 348 | .welcome-section { 349 | width: 100%; 350 | padding: 1rem; 351 | margin-bottom: 1rem; 352 | border-radius: 0.6rem; 353 | text-align: center; 354 | display: flex; 355 | justify-content: left; 356 | align-items: center; 357 | } 358 | 359 | .welcome-message { 360 | color: #fff; 361 | font-size: 3rem; 362 | font-weight: 500; 363 | font-family: 'Inter', sans-serif; 364 | margin: 0; 365 | text-align: center; 366 | } 367 | 368 | #links-container { 369 | background: rgba(0, 0, 0, 0.6); 370 | padding: 2rem; 371 | border-radius: 0.6rem; 372 | width: 90%; 373 | margin: 0 auto; 374 | flex: 1; 375 | display: flex; 376 | flex-direction: column; 377 | } 378 | 379 | .main-content { 380 | width: 100%; 381 | flex: 1; 382 | } 383 | 384 | .sections-wrapper { 385 | display: flex; 386 | gap: 2rem; 387 | width: 100%; 388 | } 389 | 390 | .links-section { 391 | flex: 1; 392 | } 393 | 394 | .calendar-section { 395 | flex: 1; 396 | } 397 | 398 | .upper-section { 399 | width: 100%; 400 | padding: 1rem; 401 | border-radius: 0.6rem; 402 | background: rgba(0, 0, 0, 0.5); 403 | } 404 | 405 | .lower-section { 406 | width: 100%; 407 | display: grid; 408 | grid-template-columns: 1fr 1fr 1fr; 409 | gap: 2rem; 410 | padding: 1rem; 411 | background: rgba(0, 0, 0, 0.5); 412 | border-radius: 0.6rem; 413 | } 414 | 415 | .column { 416 | min-width: 0; 417 | width: 100%; 418 | } 419 | 420 | /* Make sure calendar takes full width of its section */ 421 | .calendar-tabs, 422 | .calendar-content { 423 | width: 100%; 424 | background: rgba(0, 0, 0, 0.5); 425 | border-radius: 0.6rem; 426 | padding: 1rem; 427 | } 428 | 429 | .calendar-iframe { 430 | width: 100% !important; 431 | height: 70vh !important; 432 | } 433 | 434 | .main-container { 435 | background: rgba(0, 0, 0, 0.9); 436 | padding: 2rem; 437 | border-radius: 0.6rem; 438 | width: 90%; 439 | margin: 0 auto; 440 | max-width: 1800px; 441 | } 442 | 443 | .sections-wrapper { 444 | display: flex; 445 | gap: 2rem; 446 | width: 100%; 447 | } 448 | 449 | .links-section { 450 | flex: 1; /* Take up available space */ 451 | width: 50%; 452 | } 453 | 454 | .calendar-section { 455 | flex: 1; /* Take up available space */ 456 | width: 50%; 457 | } 458 | 459 | @media (max-width: 1200px) { 460 | .sections-wrapper { 461 | flex-direction: column; 462 | } 463 | 464 | .links-section, 465 | .calendar-section { 466 | width: 100%; 467 | } 468 | } 469 | 470 | /* All sections and elements - fully transparent */ 471 | .upper-section, 472 | .lower-section, 473 | .calendar-tabs, 474 | .calendar-content, 475 | .calendar-tab, 476 | .welcome-section, 477 | .category-section, 478 | .category-title, 479 | .docker-title, 480 | .docker-grid { 481 | background: rgba(0, 0, 0, 0.0); 482 | border-radius: 0.3rem; 483 | padding: 0rem; 484 | } 485 | 486 | .link-item, 487 | .docker-service, 488 | .category-title{ 489 | background: rgba(0, 0, 0, 0.3); 490 | border-radius: 0.3rem; 491 | padding: 0rem; 492 | } 493 | 494 | /* Hover states - slightly darker */ 495 | .link-item:hover, 496 | .docker-service:hover, 497 | .calendar-tab:hover { 498 | background: rgba(255, 255, 255, 0.1); 499 | transition: background 0.2s ease; 500 | } 501 | 502 | /* Active calendar tab state */ 503 | .calendar-tab.active { 504 | background: rgba(34, 139, 34, 0.5); 505 | } 506 | -------------------------------------------------------------------------------- /static/js/script.js: -------------------------------------------------------------------------------- 1 | // Time-based welcome message function 2 | function getTimeBasedMessage(messages) { 3 | const hour = new Date().getHours(); 4 | 5 | if (hour >= 5 && hour < 12) { 6 | return messages.morning; 7 | } else if (hour >= 12 && hour < 17) { 8 | return messages.afternoon; 9 | } else if (hour >= 17 && hour < 21) { 10 | return messages.evening; 11 | } else { 12 | return messages.night; 13 | } 14 | } 15 | 16 | async function loadContent() { 17 | try { 18 | const response = await fetch('links.yaml'); 19 | const yamlText = await response.text(); 20 | const data = jsyaml.load(yamlText); 21 | 22 | const container = document.getElementById('links-container'); 23 | container.className = 'main-container'; 24 | 25 | // Create main content wrapper 26 | const mainContent = document.createElement('div'); 27 | mainContent.className = 'main-content'; 28 | container.appendChild(mainContent); 29 | 30 | // Create welcome message section 31 | if (data.welcome_messages) { 32 | const welcomeSection = document.createElement('div'); 33 | welcomeSection.className = 'welcome-section'; 34 | const message = document.createElement('h1'); 35 | message.className = 'welcome-message'; 36 | message.textContent = getTimeBasedMessage(data.welcome_messages); 37 | welcomeSection.appendChild(message); 38 | mainContent.appendChild(welcomeSection); 39 | } 40 | 41 | // Create sections wrapper 42 | const sectionsWrapper = document.createElement('div'); 43 | sectionsWrapper.className = 'sections-wrapper'; 44 | mainContent.appendChild(sectionsWrapper); 45 | 46 | // Create links and calendar sections side by side 47 | const linksSection = document.createElement('div'); 48 | linksSection.className = 'links-section'; 49 | const calendarSection = document.createElement('div'); 50 | calendarSection.className = 'calendar-section'; 51 | 52 | sectionsWrapper.appendChild(linksSection); 53 | sectionsWrapper.appendChild(calendarSection); 54 | 55 | // Create upper and lower sections 56 | const upperSection = document.createElement('div'); 57 | const lowerSection = document.createElement('div'); 58 | upperSection.className = 'upper-section'; 59 | lowerSection.className = 'lower-section'; 60 | linksSection.appendChild(upperSection); 61 | linksSection.appendChild(lowerSection); 62 | 63 | // Create docker services section 64 | if (data.docker_services) { 65 | // Add title for docker services 66 | const dockerTitle = document.createElement('h2'); 67 | dockerTitle.className = 'docker-title'; 68 | dockerTitle.textContent = 'APPLICATIONS'; 69 | upperSection.appendChild(dockerTitle); 70 | 71 | const dockerGrid = document.createElement('div'); 72 | dockerGrid.className = 'docker-grid'; 73 | 74 | data.docker_services.forEach(service => { 75 | const serviceElement = document.createElement('a'); 76 | serviceElement.className = 'link-item docker-service'; 77 | serviceElement.href = service.url; 78 | serviceElement.target = '_blank'; 79 | 80 | const contentWrapper = document.createElement('div'); 81 | contentWrapper.className = 'content-wrapper'; 82 | 83 | const iconWrapper = document.createElement('div'); 84 | iconWrapper.className = 'icon-wrapper'; 85 | 86 | const icon = document.createElement('i'); 87 | icon.className = `mdi mdi-${service.icon}`; 88 | iconWrapper.appendChild(icon); 89 | 90 | const textWrapper = document.createElement('div'); 91 | textWrapper.className = 'text-wrapper'; 92 | 93 | const name = document.createElement('div'); 94 | name.className = 'link-name'; 95 | name.textContent = service.name; 96 | 97 | if (service.description) { 98 | const description = document.createElement('div'); 99 | description.className = 'link-description'; 100 | description.textContent = service.description; 101 | textWrapper.appendChild(name); 102 | textWrapper.appendChild(description); 103 | } else { 104 | textWrapper.appendChild(name); 105 | } 106 | 107 | contentWrapper.appendChild(iconWrapper); 108 | contentWrapper.appendChild(textWrapper); 109 | serviceElement.appendChild(contentWrapper); 110 | dockerGrid.appendChild(serviceElement); 111 | }); 112 | 113 | upperSection.appendChild(dockerGrid); 114 | } 115 | 116 | // Create three columns for lower section 117 | const leftColumn = document.createElement('div'); 118 | const middleColumn = document.createElement('div'); 119 | const rightColumn = document.createElement('div'); 120 | leftColumn.className = 'column'; 121 | middleColumn.className = 'column'; 122 | rightColumn.className = 'column'; 123 | lowerSection.appendChild(leftColumn); 124 | lowerSection.appendChild(middleColumn); 125 | lowerSection.appendChild(rightColumn); 126 | 127 | // Handle calendar section 128 | if (data.calendars && data.calendars.length > 0) { 129 | const tabsContainer = document.createElement('div'); 130 | tabsContainer.className = 'calendar-tabs'; 131 | 132 | const contentContainer = document.createElement('div'); 133 | contentContainer.className = 'calendar-content'; 134 | 135 | data.calendars.forEach((calendar, index) => { 136 | const tab = document.createElement('button'); 137 | tab.className = 'calendar-tab'; 138 | if (index === 0) tab.classList.add('active'); 139 | tab.textContent = calendar.name; 140 | tab.onclick = () => { 141 | document.querySelectorAll('.calendar-tab').forEach(t => t.classList.remove('active')); 142 | document.querySelectorAll('.calendar-iframe').forEach(c => c.style.display = 'none'); 143 | tab.classList.add('active'); 144 | document.getElementById(`calendar-${index}`).style.display = 'block'; 145 | }; 146 | tabsContainer.appendChild(tab); 147 | 148 | const iframe = document.createElement('iframe'); 149 | iframe.id = `calendar-${index}`; 150 | iframe.className = 'calendar-iframe'; 151 | iframe.src = calendar.url; 152 | iframe.style.display = index === 0 ? 'block' : 'none'; 153 | iframe.setAttribute('frameborder', '0'); 154 | iframe.setAttribute('loading', 'lazy'); 155 | contentContainer.appendChild(iframe); 156 | }); 157 | 158 | calendarSection.appendChild(tabsContainer); 159 | calendarSection.appendChild(contentContainer); 160 | } 161 | 162 | // Group links by category 163 | const linksByCategory = {}; 164 | data.links.forEach(link => { 165 | if (!linksByCategory[link.category]) { 166 | linksByCategory[link.category] = []; 167 | } 168 | linksByCategory[link.category].push(link); 169 | }); 170 | 171 | // Function to create category section 172 | const createCategorySection = (category) => { 173 | if (linksByCategory[category]) { 174 | const categorySection = document.createElement('div'); 175 | categorySection.className = 'category-section'; 176 | 177 | const categoryTitle = document.createElement('h2'); 178 | categoryTitle.className = 'category-title'; 179 | categoryTitle.textContent = category; 180 | categorySection.appendChild(categoryTitle); 181 | 182 | linksByCategory[category].forEach(link => { 183 | const linkElement = document.createElement('a'); 184 | linkElement.className = 'link-item'; 185 | linkElement.href = link.url; 186 | linkElement.target = '_blank'; 187 | 188 | const contentWrapper = document.createElement('div'); 189 | contentWrapper.className = 'content-wrapper'; 190 | 191 | const iconWrapper = document.createElement('div'); 192 | iconWrapper.className = 'icon-wrapper'; 193 | iconWrapper.style.width = '24px'; 194 | iconWrapper.style.display = 'inline-flex'; 195 | iconWrapper.style.justifyContent = 'center'; 196 | 197 | const icon = document.createElement('i'); 198 | icon.className = `mdi mdi-${link.icon}`; 199 | iconWrapper.appendChild(icon); 200 | 201 | const textWrapper = document.createElement('div'); 202 | textWrapper.className = 'text-wrapper'; 203 | 204 | const name = document.createElement('div'); 205 | name.className = 'link-name'; 206 | name.textContent = link.name; 207 | 208 | if (link.description) { 209 | const description = document.createElement('div'); 210 | description.className = 'link-description'; 211 | description.textContent = link.description; 212 | textWrapper.appendChild(name); 213 | textWrapper.appendChild(description); 214 | } else { 215 | textWrapper.appendChild(name); 216 | } 217 | 218 | contentWrapper.appendChild(iconWrapper); 219 | contentWrapper.appendChild(textWrapper); 220 | linkElement.appendChild(contentWrapper); 221 | categorySection.appendChild(linkElement); 222 | }); 223 | 224 | return categorySection; 225 | } 226 | return null; 227 | }; 228 | 229 | // Populate columns based on configuration 230 | if (data.columns) { 231 | // Left column 232 | if (data.columns.left) { 233 | data.columns.left.forEach(category => { 234 | const section = createCategorySection(category); 235 | if (section) leftColumn.appendChild(section); 236 | }); 237 | } 238 | 239 | // Middle column 240 | if (data.columns.middle) { 241 | data.columns.middle.forEach(category => { 242 | const section = createCategorySection(category); 243 | if (section) middleColumn.appendChild(section); 244 | }); 245 | } 246 | 247 | // Right column 248 | if (data.columns.right) { 249 | data.columns.right.forEach(category => { 250 | const section = createCategorySection(category); 251 | if (section) rightColumn.appendChild(section); 252 | }); 253 | } 254 | } 255 | 256 | } catch (error) { 257 | console.error('Error loading content:', error); 258 | } 259 | } 260 | 261 | document.addEventListener('DOMContentLoaded', loadContent); -------------------------------------------------------------------------------- /static/links.yaml: -------------------------------------------------------------------------------- 1 | # Docker Services Section 2 | # List your local applications/services here 3 | # - name: Display name of the service 4 | # - url: Local IP or domain where the service is hosted 5 | # - icon: Material Design Icon name (without the mdi- prefix) 6 | # - description: Brief description of the service 7 | docker_services: 8 | - name: "CASAOS" 9 | url: "http://192.168.1.123" 10 | icon: "docker" 11 | description: "Local services" 12 | - name: "ExcelDraw" 13 | url: "http://192.168.1.123" 14 | icon: "docker" 15 | description: "Local services" 16 | 17 | # Column Layout Configuration 18 | # Define which categories appear in each column 19 | # - left: Categories to appear in the left column 20 | # - middle: Categories to appear in the middle column 21 | # - right: Categories to appear in the right column 22 | columns: 23 | left: 24 | - Communication 25 | - Entertainment 26 | middle: 27 | - Local Services 28 | right: 29 | - Profiles 30 | - Tools 31 | - Productivity 32 | 33 | # Categories List 34 | # Define all available categories 35 | # These should match the category names used in the links section 36 | categories: 37 | - Profiles 38 | - Communication 39 | - Entertainment 40 | - Productivity 41 | - Tools 42 | - Local Services 43 | 44 | # Links Section 45 | # List all your links here 46 | # - name: Display name of the link 47 | # - url: Web address 48 | # - icon: Material Design Icon name (without the mdi- prefix) https://pictogrammers.com/library/mdi/ 49 | # - description: Brief description of the link 50 | # - category: Must match one of the categories defined above 51 | links: 52 | - name: GitHub 53 | url: https://github.com 54 | icon: github 55 | description: "Personal code repositories and contributions" 56 | category: Profiles 57 | 58 | - name: Linkedin 59 | url: https://linkedin.com 60 | icon: linkedin 61 | description: "Professional network and CV" 62 | category: Profiles 63 | 64 | - name: ResearchGate 65 | url: https://researchgate.net 66 | icon: school 67 | description: "Academic publications and research" 68 | category: Profiles 69 | 70 | - name: Personal Gmail 71 | url: https://mail.google.com 72 | icon: gmail 73 | description: "Personal email account" 74 | category: Communication 75 | 76 | - name: College Outlook 77 | url: https://outlook.office.com/mail/ 78 | icon: microsoft-outlook 79 | description: "College email account" 80 | category: Communication 81 | 82 | - name: Slack 83 | url: https://slack.com 84 | icon: slack 85 | description: "Team communication platform" 86 | category: Communication 87 | 88 | - name: Netflix 89 | url: https://netflix.com 90 | icon: netflix 91 | description: "Streaming service" 92 | category: Entertainment 93 | 94 | - name: YouTube 95 | url: https://youtube.com 96 | icon: youtube 97 | description: "Video streaming service" 98 | category: Entertainment 99 | 100 | - name: Spotify 101 | url: https://spotify.com 102 | icon: movie 103 | description: "Music streaming service" 104 | category: Entertainment 105 | 106 | - name: Google Drive 107 | url: https://drive.google.com 108 | icon: folder 109 | description: "Cloud storage and file sharing" 110 | category: Productivity 111 | 112 | - name: Google Docs 113 | url: https://docs.google.com 114 | icon: folder 115 | description: "Cloud storage and file sharing" 116 | category: Productivity 117 | 118 | - name: ExcelDraw 119 | url: http://192.168.1.123 120 | icon: docker 121 | description: "Local services" 122 | category: Local Services 123 | 124 | # Calendar Section 125 | # Configure Google Calendar embeds 126 | # - name: Display name for the calendar tab 127 | # - url: Google Calendar embed URL 128 | calendars: 129 | - name: "Calendar 1" 130 | url: "https://calendar.google.com/calendar/embed?src=youremail@gmail.com&bgcolor=%23000000&color=%23ffffff&showTitle=0&showNav=1&showDate=1&showPrint=0&showTabs=1&showCalendars=0&showTz=1&mode=WEEK&ctz=America%2FNew_York" 131 | - name: "Calendar 2" 132 | url: "https://calendar.google.com/calendar/embed?src=your_email@gmail.com&bgcolor=%23000000&color=%23ffffff&showTitle=0&showNav=1&showDate=1&showPrint=0&showTabs=1&showCalendars=0&showTz=1&mode=WEEK&ctz=America%2FNew_York" 133 | 134 | # Welcome Messages 135 | # Configure different messages for different times of day 136 | # Times are: 137 | # - Morning: 5:00 AM - 11:59 AM 138 | # - Afternoon: 12:00 PM - 4:59 PM 139 | # - Evening: 5:00 PM - 8:59 PM 140 | # - Night: 9:00 PM - 4:59 AM 141 | welcome_messages: 142 | morning: "Good Morning!" 143 | afternoon: "Good Afternoon!" 144 | evening: "Good Evening!" 145 | night: "Good Night!" -------------------------------------------------------------------------------- /static/uploads/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/securemindorg/SimpleDash/7abebbd1337da9e8130b8f927029e2c5e9fbb596/static/uploads/background.jpg -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SimpleDash 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------