🌐

KapaDW

Plataforma de Gestão de Serviços Web

Relatório Técnico do Projeto Prático

InstituiçãoInstituto Politécnico de Tomar (IPT)
CursoLicenciatura em Engenharia Informática
Unidade CurricularDesenvolvimento Web
Ano / Semestre2º Ano, 2º Semestre
Ano Letivo2024 / 2025
AutoresDiogo Godinho (Nº 27220)
Kanstantsin Khomchanka (Nº 27230)
DataMaio de 2025
Repositóriogithub.com/diogogodinhowaze/projetodw

📋 Índice

  1. Introdução e Objetivos
  2. Arquitetura do Sistema
    1. Visão Geral
    2. Estrutura de Ficheiros
    3. Fluxo de uma Operação
  3. Modelos de Dados
    1. ApplicationUser
    2. Servico e CategoriaServico
    3. Pedido e PedidoServico
    4. Mensagem
    5. PedidoAlteracao
    6. Enumerações
  4. Base de Dados e ORM
    1. ApplicationDbContext
    2. Relações e Constraints
    3. Seed de Dados
  5. Autenticação e Autorização
    1. ASP.NET Core Identity
    2. Políticas e Convenções
    3. Segurança de Passwords
  6. Aplicação Web — Razor Pages
    1. Gestão de Pedidos
    2. Catálogo de Serviços
    3. Gestão de Categorias
    4. Gestão de Clientes
    5. Sistema de Alterações Propostas
    6. Chat por Pedido
  7. API REST
    1. PedidosController
    2. ServicosController
    3. DTOs e Validação
    4. Swagger / OpenAPI
  8. Comunicação em Tempo Real — SignalR
  9. Interface e Estética
    1. Layout Global
    2. Stack de Estilização
    3. Componentes Visuais
    4. Página de Início
  10. Publicação e Infraestrutura
    1. Contentorização com Docker
    2. Deployment na Render.com
    3. Pipeline de Entrega Contínua
  11. Tecnologias e Dependências
  12. Padrões de Desenvolvimento
  13. Dificuldades e Soluções
  14. Melhorias e Evolução da Plataforma
  15. Conclusão
  16. Referências

1. Introdução e Objetivos

O projeto KapaDW foi desenvolvido no âmbito da unidade curricular de Desenvolvimento Web da Licenciatura em Engenharia Informática do Instituto Politécnico de Tomar. O objetivo central foi conceber e implementar uma plataforma completa de gestão de serviços web que demonstre a aplicação prática dos conteúdos lecionados ao longo do semestre.

A plataforma simula o modelo de negócio de uma agência digital: clientes registam-se na plataforma, consultam o catálogo de serviços disponíveis (desenvolvimento de websites, lojas online, SEO, entre outros), submetem pedidos com requisitos específicos e acompanham o progresso dos seus projetos em tempo real. Do lado da administração, o gestor tem visibilidade total sobre todos os pedidos, pode gerir o ciclo de vida de cada um, comunicar com os clientes através de um sistema de chat integrado, e gerir os próprios serviços e categorias oferecidos.

O projeto vai além de uma aplicação CRUD simples: inclui um sistema de notificações em tempo real baseado em WebSockets (SignalR), um workflow de aprovação de alterações para pedidos já submetidos, uma API REST documentada com Swagger, e um pipeline de publicação automatizado com Docker e Render.com, garantindo disponibilidade contínua.

Objetivos técnicos alcançados:

Este relatório descreve em detalhe todos os componentes do sistema: arquitetura, modelos de dados, funcionalidades, interface, segurança e infraestrutura de publicação.

2. Arquitetura do Sistema

2.1 Visão Geral

O sistema é composto por dois projetos ASP.NET Core 8 que partilham a mesma base de dados SQLite e o mesmo conjunto de modelos de domínio. Esta separação segue o princípio da responsabilidade única: a aplicação web trata da interface com o utilizador, enquanto a API fornece acesso programático aos dados para integrações externas.

🖥️ WebServicos Razor Pages
Porta 7001 (local)
Porta 10000 (cloud)

Bootstrap 5 SignalR JS jQuery
💾 SQLite webservicos.db
Entity Framework Core
Migrations automáticas

8 tabelas Seed Data
📡 WebServicos.API MVC Controllers
REST + Swagger
Porta 7002 (local)
Porta 10000 (cloud)

OpenAPI JSON

Figura 1 — Arquitetura geral do sistema com os dois projetos e a base de dados partilhada

O projeto API referencia diretamente o projeto WebServicos como dependência (via <ProjectReference> no ficheiro .csproj), o que lhe permite reutilizar os modelos (Models.cs), o contexto de base de dados (ApplicationDbContext), as migrations e as entidades Identity, sem duplicação de código.

2.2 Estrutura de Ficheiros

Pasta / FicheiroDescrição
WebServicos/WebServicos/Projeto principal — aplicação web Razor Pages
WebServicos/WebServicos.API/Projeto API REST com Swagger
Models/Models.csTodas as entidades do domínio
Data/ApplicationDbContext.csContexto EF Core, relações, seed de dados
Data/DbSeeder.csCriação de roles e contas padrão
Hubs/NotificacoesHub.csHub SignalR para notificações em tempo real
Pages/26 ficheiros Razor Pages (.cshtml + .cshtml.cs)
Pages/Shared/_Layout.cshtmlTemplate global com navbar, footer e SignalR JS
Controllers/ (API)PedidosController + ServicosController
wwwroot/css/app.cssCSS gerado pelo Tailwind CSS (minificado)
DockerfileImagem Docker para o projeto web
Dockerfile.apiImagem Docker para a API
render.yamlBlueprint de deployment na Render.com

2.3 Fluxo de uma Operação Típica

O ciclo de vida de um pedido ilustra como os vários componentes interagem entre si:

1Login
2Catalogo
3Pedido
4SignalR
5Admin
6Chat
7Entrega

Figura 2 — Fluxo típico de um pedido desde o registo até à entrega

  1. O cliente autentica-se via formulário Identity (/Identity/Account/Login)
  2. Consulta o catálogo de serviços (/Servicos), filtrável por pesquisa de texto
  3. Submete um pedido (/Pedidos/Create) selecionando serviços e preenchendo requisitos
  4. A aplicação aciona o hub SignalR para notificar o administrador em tempo real
  5. O administrador analisa o pedido (/Pedidos/Details), atualiza estados e comunica via chat
  6. O cliente e o admin trocam mensagens no chat integrado ao pedido
  7. O admin marca como Concluído e introduz o URL de entrega do projeto

3. Modelos de Dados

Todos os modelos estão definidos no ficheiro WebServicos/WebServicos/Models/Models.cs, dentro do namespace WebServicos.Models. O ficheiro contém 7 classes e 2 enumerações, cobrindo todas as entidades do domínio.

3.1 ApplicationUser

Estende a classe IdentityUser do ASP.NET Core Identity, adicionando campos específicos da plataforma:

PropriedadeTipoValidaçãoDescrição
NomeCompletostringRequired, MaxLength(150)Nome completo do utilizador
DataRegistoDateTimeDefault: UTC.NowData de registo na plataforma
PedidosICollection<Pedido>NavigationPedidos submetidos pelo utilizador

A herança de IdentityUser acrescenta automaticamente campos como Id (GUID), Email, PasswordHash, SecurityStamp, LockoutEnd, entre outros, geridos pelo framework.

3.2 Servico e CategoriaServico

A entidade Servico representa cada tipo de trabalho oferecido pela agência. Tem uma relação opcional com CategoriaServico (chave estrangeira nullable), o que permite que serviços existam sem categoria atribuída:

PropriedadeTipoValidaçãoDescrição
IdintPK auto-incrementIdentificador único
NomestringRequired, Max(100)Nome do serviço
DescricaostringRequired, Max(1000)Descrição detalhada
PrecoBasedecimal(10,2)Range(0, 99999.99)Preço base em EUR
AtivoboolDefault: trueVisível para clientes
DataCriacaoDateTimeDefault: UTC.NowTimestamp de criação
IconestringMax(50)Classe Bootstrap Icons (ex: "bi-globe")
CategoriaServicoIdint?FK nullableCategoria a que pertence

Foram inseridos 6 serviços pré-definidos via seed data, com preços reais de mercado:

NomePreço BaseÍconeCategoria
Website Institucional€ 799,00bi-globeWebsites
Loja Online€ 1.499,00bi-shopE-commerce
Landing Page€ 349,00bi-layout-text-windowWebsites
Otimização SEO€ 299,00bi-searchSEO & Marketing
Manutenção Mensal€ 89,00bi-toolsManutenção
Recuperação de Software & Hardware€ 3.000,00bi-pc-displayHardware & Software

3.3 Pedido e PedidoServico

O Pedido é a entidade central do sistema, representando um projeto contratado por um cliente. A relação com os serviços é muitos-para-muitos, implementada através da tabela de junção PedidoServico, que acrescenta campos próprios (quantidade, preço acordado, notas por serviço):

Propriedade (Pedido)TipoDescrição
TituloProjetostring (Max 200)Nome do projeto
Descricaostring (Max 2000)Requisitos detalhados
EstadoEstadoPedidoEstado atual no workflow
OrcamentoTotaldecimal?Soma dos preços dos serviços no momento da criação
PrazoEstimadoDateTime?Data desejada de conclusão (opcional)
EnderecoHttpstring? (URL)URL do projeto entregue (preenchido pelo admin)
ClienteIdstring FKReferência ao ApplicationUser
Propriedade (PedidoServico)TipoDescrição
PedidoId + ServicoIdint + intChave primária composta
Quantidadeint (1-100)Unidades do serviço contratadas
PrecoAcordadodecimal?Preço negociado (pode diferir do PrecoBase)
Notasstring? (Max 500)Requisitos específicos para este serviço

3.4 Mensagem

A entidade Mensagem suporta o sistema de chat integrado a cada pedido. Cada mensagem pertence a um único pedido e tem um remetente identificado:

PropriedadeTipoDescrição
PedidoIdint FKPedido a que pertence esta mensagem
RemetenteIdstring FKUtilizador que enviou a mensagem
Conteudostring (Max 2000)Texto da mensagem
DataHoraDateTimeTimestamp de envio (UTC)
LidaboolMarcado como lido ao abrir o chat

3.5 PedidoAlteracao

Esta entidade suporta o fluxo de alterações propostas pelo cliente em pedidos já submetidos. Em vez de editar diretamente os dados, o cliente propõe alterações que o administrador aprova ou rejeita:

PropriedadeTipoDescrição
TituloProjetoPropostostring?Novo título proposto (opcional)
DescricaoPropostastring?Nova descrição proposta
ObservacoesPropostastring?Novas observações propostas
EstadoEstadoAlteracaoPendente / Aprovado / Rejeitado
MotivoRejeicaostring?Explicação da rejeição pelo admin
DataDecisaoDateTime?Data da decisão do admin
DecididoPorIdstring? FKAdmin que tomou a decisão

3.6 Enumerações

O domínio usa duas enumerações para modelar o estado de pedidos e alterações, respetivamente:

EstadoPedido

ValorIntSignificado
Pendente0Recebido, por analisar
EmAnalise1Admin a analisar
Aprovado2Aprovado, aguarda início
EmDesenvolvimento3Em desenvolvimento
EmRevisao4Aguarda revisão do cliente
Concluido5Entregue e aceite
Cancelado6Cancelado

EstadoAlteracao

ValorIntSignificado
Pendente0Aguarda decisão do admin
Aprovado1Aprovado e aplicado
Rejeitado2Rejeitado com motivo

4. Base de Dados e ORM

4.1 ApplicationDbContext

O ApplicationDbContext herda de IdentityDbContext<ApplicationUser>, integrando automaticamente as tabelas do ASP.NET Identity (AspNetUsers, AspNetRoles, AspNetUserRoles, etc.) com as tabelas de domínio da aplicação. A classe está definida em Data/ApplicationDbContext.cs.

Os DbSet expostos são:

Propriedade DbSetTabela gerada
DbSet<Servico> ServicosServicos
DbSet<CategoriaServico> CategoriasServicoCategoriasServico
DbSet<Pedido> PedidosPedidos
DbSet<PedidoServico> PedidoServicosPedidoServicos
DbSet<Mensagem> MensagensMensagens
DbSet<PedidoAlteracao> PedidoAlteracoesPedidoAlteracoes

4.2 Relações e Constraints

O método OnModelCreating configura as relações entre entidades, incluindo chaves compostas e comportamentos de cascata:

// Chave primária composta da tabela de junção
modelBuilder.Entity<PedidoServico>()
    .HasKey(ps => new { ps.PedidoId, ps.ServicoId });

// Pedido → User: Restrict (não apaga utilizador com pedidos)
modelBuilder.Entity<Pedido>()
    .HasOne(p => p.Cliente)
    .WithMany(u => u.Pedidos)
    .HasForeignKey(p => p.ClienteId)
    .OnDelete(DeleteBehavior.Restrict);

// Servico → Categoria: SetNull (categoria pode ser eliminada)
modelBuilder.Entity<Servico>()
    .HasOne(s => s.CategoriaServico)
    .WithMany(c => c.Servicos)
    .HasForeignKey(s => s.CategoriaServicoId)
    .OnDelete(DeleteBehavior.SetNull);

A escolha de DeleteBehavior.Restrict para a relação Pedido→Utilizador protege a integridade dos dados: um utilizador com pedidos associados não pode ser eliminado diretamente. Já o DeleteBehavior.SetNull na relação Serviço→Categoria permite eliminar categorias sem perder serviços, colocando simplesmente o campo FK a NULL.

4.3 Seed de Dados

O DbSeeder.cs é responsável pela criação automática de roles e contas padrão na primeira execução. É chamado no arranque da aplicação (Program.cs) e é idempotente: pode ser executado múltiplas vezes sem criar duplicados.

public static async Task SeedAsync(IServiceProvider serviceProvider)
{
    // Criar roles se não existirem
    await roleManager.CreateAsync(new IdentityRole(RoleAdmin));
    await roleManager.CreateAsync(new IdentityRole(RoleCliente));

    // Criar conta admin padrão
    var admin = new ApplicationUser {
        Email = "[email protected]",
        NomeCompleto = "Administrador do Sistema",
        EmailConfirmed = true
    };
    await userManager.CreateAsync(admin, "Admin@123");
    await userManager.AddToRoleAsync(admin, RoleAdmin);
}

Os dados de seed de categorias e serviços são inseridos diretamente no OnModelCreating via modelBuilder.Entity<T>().HasData(...), que garante que são aplicados como parte das migrations do EF Core.

5. Autenticação e Autorização

5.1 ASP.NET Core Identity

A autenticação é gerida inteiramente pelo ASP.NET Core Identity, que fornece out-of-the-box: registo, login, logout, recuperação de password, bloqueio de conta e gestão de roles. A integração é feita através do scaffolding do Identity UI, que gera automaticamente as páginas Razor em /Identity/Account/.

A aplicação está configurada com dois roles: Administrador e Cliente. Cada utilizador pertence a exatamente um role, que determina as páginas e ações a que tem acesso.

5.2 Políticas e Convenções de Autorização

A autorização é configurada em Program.cs com uma combinação de convenções por pasta e atributos por classe:

// Convenções por pasta/página
options.Conventions.AuthorizeFolder("/Pedidos");
options.Conventions.AuthorizeFolder("/Clientes", "Administrador");
options.Conventions.AuthorizePage("/Servicos/Create", "Administrador");
options.Conventions.AuthorizePage("/Servicos/Edit", "Administrador");
options.Conventions.AuthorizePage("/Servicos/Delete", "Administrador");

// Políticas nomeadas
options.AddPolicy("Administrador", p => p.RequireRole("Administrador"));
options.AddPolicy("Cliente", p => p.RequireRole("Cliente", "Administrador"));

Além das convenções globais, cada PageModel de áreas sensíveis usa o atributo [Authorize(Roles = "Administrador")] como segunda linha de defesa. Algumas páginas fazem verificações adicionais em runtime — por exemplo, em Pedidos/Edit, um cliente só pode editar pedidos no estado Pendente e o acesso a pedidos de outros clientes é bloqueado com Forbid().

5.3 Segurança de Passwords e Sessão

ParâmetroValor
Comprimento mínimo8 caracteres
Dígito obrigatórioSim (0–9)
Maiúscula obrigatóriaSim (A–Z)
Caráter especial obrigatórioSim (!@#$%^&*)
Tentativas falhadas máximas5
Duração do bloqueio15 minutos
Duração da sessão (cookie)8 horas
Confirmação de emailDesativada (demo)
Redirect não autenticado/Identity/Account/Login
Redirect acesso negado/Erro/AcessoNegado

6. Aplicação Web — Razor Pages

A aplicação web segue o padrão Razor Pages, onde cada página tem um ficheiro .cshtml (view) e um ficheiro .cshtml.cs (page model com a lógica). Os handlers OnGet, OnPost e variantes nomeadas (OnPostAprovarAsync, OnPostAlterarEstadoAsync) implementam as ações de cada página. No total existem 26 ficheiros .cshtml em 7 áreas funcionais.

6.1 Gestão de Pedidos

A área de pedidos é o núcleo funcional da plataforma, com 7 páginas distintas:

PáginaAcessoFuncionalidade Principal
Pedidos/IndexAutenticadoLista pedidos; admin vê todos, cliente vê só os seus. Carregamento com Include (eager loading) para evitar N+1 queries.
Pedidos/CreateClienteFormulário de criação com seleção múltipla de serviços. Calcula orçamento automaticamente. Envolve criação em transação. Dispara notificação SignalR.
Pedidos/DetailsAutenticadoVisualização detalhada. Admin pode alterar estado e a aplicação notifica o cliente via SignalR direcionado.
Pedidos/EditAutenticadoCliente propõe alterações (via PedidoAlteracao); admin edita diretamente (prazo, estado, URL entrega).
Pedidos/DeleteAutenticadoCliente só cancela se Pendente; admin pode cancelar qualquer estado.
Pedidos/ChatAutenticadoChat por pedido: envia mensagens, marca como lidas ao abrir, mostra histórico ordenado cronologicamente.
Pedidos/RevisarAlteracoesAdminLista e detalha propostas de alteração pendentes. Admin aprova (aplica ao pedido) ou rejeita (com motivo obrigatório).

Lógica de Criação com Transação

O handler OnPostAsync de Pedidos/Create usa uma transação explícita para garantir atomicidade:

await using var transaction = await _db.Database.BeginTransactionAsync();
try {
    _db.Pedidos.Add(Pedido);
    await _db.SaveChangesAsync();
    // Adicionar entradas na tabela de junção PedidoServico
    foreach (var id in ServicosIds) {
        _db.PedidoServicos.Add(new PedidoServico { PedidoId = Pedido.Id, ServicoId = id });
    }
    await _db.SaveChangesAsync();
    await transaction.CommitAsync();
} catch {
    await transaction.RollbackAsync(); throw;
}

6.2 Catálogo de Serviços

A área de serviços tem comportamento diferenciado por role: clientes veem apenas serviços ativos; administradores veem todos (incluindo inativos), com indicação visual do estado. As páginas de criação, edição e eliminação são exclusivas de administradores.

A pesquisa no catálogo funciona via query string ?Pesquisa=... e filtra por Nome e Descricao com Contains() (LIKE no SQL). O resultado é ordenado por nome.

A eliminação de um serviço verifica primeiro se existem pedidos associados em PedidoServicos; se sim, bloqueia a operação com uma mensagem de erro clara, evitando violações de integridade referencial.

6.3 Gestão de Categorias

As categorias servem para organizar o catálogo de serviços. O CRUD completo (Index, Create, Edit, Delete) está disponível apenas para administradores. Na eliminação de uma categoria, os serviços associados mantêm-se na base de dados mas ficam sem categoria (CategoriaServicoId = NULL), graças ao comportamento SetNull configurado no contexto.

6.4 Gestão de Clientes

A página Clientes/Index apresenta ao administrador uma tabela com todos os utilizadores registados, incluindo o número total de pedidos, a data de registo e indicação de se é administrador. A verificação de role é feita via UserManager.IsInRoleAsync() para cada utilizador.

Nota de performance: A verificação de role por utilizador resulta em N queries adicionais ao Identity (uma por utilizador). Numa aplicação de produção com muitos utilizadores, esta secção beneficiaria de um JOIN direto à tabela AspNetUserRoles.

6.5 Sistema de Alterações Propostas

Um dos aspetos mais interessantes do sistema é o workflow de alterações. Um cliente com um pedido em curso pode propor alterações ao título, descrição ou observações sem as aplicar diretamente. A proposta fica no estado Pendente até o administrador a analisar em Pedidos/RevisarAlteracoes.

ClientePropõe
BDPedidoAlteracao
Pendente
AdminRevisa
✓ AprovadoAplica ao Pedido
|
✗ RejeitadoCom motivo

Figura 3 — Fluxo de aprovação de alterações propostas

6.6 Chat por Pedido

Cada pedido tem um chat privado entre o cliente e o administrador. As mensagens são persistidas na tabela Mensagens com controlo de estado de leitura (Lida). Ao abrir o chat, todas as mensagens não lidas são marcadas automaticamente como lidas via update em batch. O histórico é apresentado em ordem cronológica, com diferenciação visual entre mensagens enviadas e recebidas.

7. API REST

O projeto WebServicos.API expõe uma API RESTful documentada com Swagger, partilhando a base de dados com a aplicação web. É útil para integrações externas, dashboards de monitorização, ou acesso programático aos dados sem necessidade de navegar pela interface web.

7.1 PedidosController

Rota base: /api/pedidos

GET /api/pedidos Lista pedidos com filtros opcionais por clienteId e estado
GET /api/pedidos/{id} Detalhe completo de um pedido com serviços e cliente
PATCH /api/pedidos/{id}/estado Atualiza apenas o estado do pedido (admin)

O endpoint GET /api/pedidos aceita dois query parameters opcionais: clienteId (filtra por utilizador) e estado (filtra pelo valor numérico do enum EstadoPedido). A resposta é uma projeção anónima que inclui dados do cliente e a lista de serviços associados, ordenada por data decrescente.

7.2 ServicosController

Rota base: /api/servicos

GET /api/servicos Lista serviços, com filtro opcional por ativo=true/false
GET /api/servicos/{id} Detalhe de um serviço pelo ID
POST /api/servicos Cria novo serviço (retorna 201 Created com Location header)
PUT /api/servicos/{id} Atualiza serviço completo (retorna 204 No Content)
DELETE /api/servicos/{id} Elimina serviço; 409 Conflict se tiver pedidos associados
GET /api/servicos/estatisticas Agrega TotalServicos, TotalAtivos, PrecoMedio, usos por serviço

O endpoint DELETE inclui proteção de integridade: antes de eliminar, verifica se existem entradas em PedidoServicos e retorna 409 Conflict se sim, com mensagem explicativa. O endpoint de estatísticas é particularmente útil para dashboards externos, retornando dados agregados numa única chamada.

7.3 DTOs e Validação

A API utiliza Data Transfer Objects (DTOs) para separar os modelos de domínio da representação HTTP. O ServicoDto é usado para leitura e o CreateServicoDto para escrita (POST e PUT), com validação via Data Annotations:

public class CreateServicoDto
{
    [Required] [StringLength(100)]
    public string Nome { get; set; } = "";

    [Required]
    public string Descricao { get; set; } = "";

    [Range(0, 99999.99)]
    public decimal PrecoBase { get; set; }

    public bool Ativo { get; set; } = true;
    public string Icone { get; set; } = "bi-globe";
}

7.4 Swagger / OpenAPI

A documentação da API é gerada automaticamente pelo Swashbuckle.AspNetCore (v6.5.0). Em ambiente de produção, o Swagger UI permanece ativo para facilitar a demonstração académica. A interface está acessível em /index.html e permite testar todos os endpoints diretamente no browser, sem necessidade de ferramentas externas como Postman.

A configuração CORS está definida como AllowAll para permitir chamadas de qualquer origem, adequado para contexto académico.

8. Comunicação em Tempo Real — SignalR

O SignalR é uma biblioteca ASP.NET Core que simplifica a adição de funcionalidades em tempo real usando WebSockets (com fallback para long-polling quando WebSockets não estão disponíveis). Na plataforma KapaDW, é usado para dois cenários:

Hub: NotificacoesHub

O hub está registado no endpoint /hub/notificacoes e requer autenticação ([Authorize]):

/// Notifica todos os clientes conectados de um novo pedido
public async Task NotificarNovoPedido(string tituloProjeto, string nomeCliente)
{
    await Clients.All.SendAsync("NovoPedido", tituloProjeto, nomeCliente,
        DateTime.Now.ToString("HH:mm"));
}

/// Notifica apenas o cliente específico sobre atualização do seu pedido
public async Task NotificarAtualizacaoPedido(string clienteId,
    string tituloProjeto, string novoEstado)
{
    await Clients.User(clienteId).SendAsync("PedidoAtualizado",
        tituloProjeto, novoEstado, DateTime.Now.ToString("HH:mm"));
}

No frontend (_Layout.cshtml), o cliente SignalR JavaScript está configurado com reconexão automática e um sistema de toasts que apresenta as notificações no canto superior direito com animação, desaparecendo automaticamente após 5 segundos:

connection.on("NovoPedido", (titulo, cliente, hora) => {
    mostrarNotificacao(`Novo pedido: "${titulo}" de ${cliente} às ${hora}`);
    incrementarBadge();
});

connection.on("PedidoAtualizado", (titulo, estado, hora) => {
    mostrarNotificacao(`O pedido "${titulo}" foi atualizado: ${estado} (${hora})`);
});

A integração no fluxo de criação de pedido (Pedidos/Create) é feita server-side através da injeção do hub no page model:

private readonly IHubContext<NotificacoesHub> _hub;

// Após guardar o pedido com sucesso:
await _hub.Clients.All.SendAsync("NovoPedido", Pedido.TituloProjeto, nomeCliente);

9. Interface e Estética

9.1 Layout Global

Toda a aplicação utiliza um template único definido em Pages/Shared/_Layout.cshtml. O layout define a estrutura consistente de todas as páginas: navbar no topo, área de conteúdo central e footer. A fonte utilizada é Inter (Google Fonts), uma das fontes sans-serif mais usadas em produtos digitais modernos pela sua legibilidade em ecrãs.

A navbar adapta o seu conteúdo ao utilizador autenticado e ao seu role:

Item de MenuVisível ParaRota
InícioTodos/Index
ServiçosTodos/Servicos/Index
PedidosAutenticados/Pedidos/Index
Administração (dropdown)Administradores— (contém sub-itens)
  ↳ ClientesAdmin/Clientes/Index
  ↳ CategoriasAdmin/Categorias/Index
  ↳ Novo ServiçoAdmin/Servicos/Create
SobreTodos/Sobre
Notificações (sino)Autenticados— (SignalR toast)

9.2 Stack de Estilização

A interface usa uma abordagem híbrida com dois frameworks CSS complementares:

O Tailwind é compilado em tempo de build via um target MSBuild personalizado no .csproj:

<Target Name="BuildTailwind" BeforeTargets="Build"
        Condition="Exists('package.json') And '$(SkipNpmBuild)' != 'true'">
    <Exec Command="npm run build" WorkingDirectory="$(ProjectDir)" />
</Target>

A configuração do Tailwind em tailwind.config.js define uma paleta de cores customizada para o tema dark da homepage:

VariávelValorUso
ws-bg#070b14Fundo principal da homepage
ws-surface#111827Superfície de cards
ws-blue#3b82f6Cor de destaque principal
ws-purple#8b5cf6Cor de destaque secundária
ws-cyan#06b6d4Cor de destaque terciária
ws-text#f1f5f9Texto principal (dark mode)
ws-muted#64748bTexto secundário

9.3 Componentes Visuais Notáveis

Ao longo da interface, destacam-se vários padrões de design consistentes:

9.4 Página de Início

A página inicial (Pages/Index.cshtml) tem um design distinto do restante da aplicação, com tema escuro e interativo. Inclui uma animação de partículas tech renderizada em canvas HTML5, onde partículas se movem e formam conexões ao aproximar o rato, criando uma estética futurista associada a tecnologia. A section hero usa a paleta Tailwind dark (ws-bg, ws-blue) com texto em gradiente e call-to-action buttons em destaque.

10. Publicação e Infraestrutura

10.1 Contentorização com Docker

Ambos os projetos são contentorizados com ficheiros Dockerfile separados, usando o padrão multi-stage build para produzir imagens leves de produção.

Dockerfile — Aplicação Web

# Stage 1: Build com SDK + Node.js (para compilar Tailwind CSS)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs

WORKDIR /src
COPY WebServicos/WebServicos/package.json WebServicos/WebServicos/
RUN cd WebServicos/WebServicos && npm install

COPY WebServicos/WebServicos/WebServicos.csproj WebServicos/WebServicos/
RUN dotnet restore WebServicos/WebServicos/WebServicos.csproj

COPY . .
WORKDIR /src/WebServicos/WebServicos
RUN dotnet publish -c Release -o /app/publish --nologo

# Stage 2: Runtime — imagem mínima sem SDK
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 10000
ENV ASPNETCORE_URLS=http://+:10000
ENTRYPOINT ["dotnet", "WebServicos.dll"]

A inclusão do Node.js na stage de build é necessária porque o target MSBuild BuildTailwind executa npm run build durante o dotnet publish, gerando o ficheiro wwwroot/css/app.css minificado.

Dockerfile.api — API REST

# Stage 1: Build (sem Node.js — SkipNpmBuild=true)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY WebServicos/WebServicos/WebServicos.csproj WebServicos/WebServicos/
COPY WebServicos/WebServicos.API/WebServicos.API.csproj WebServicos/WebServicos.API/
RUN dotnet restore WebServicos/WebServicos.API/WebServicos.API.csproj

COPY . .
WORKDIR /src/WebServicos/WebServicos.API
RUN dotnet publish -c Release -o /app/publish --nologo -p:SkipNpmBuild=true

# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 10000
ENV ASPNETCORE_URLS=http://+:10000
ENTRYPOINT ["dotnet", "WebServicos.API.dll"]

O flag -p:SkipNpmBuild=true é crucial: a API referencia o projeto WebServicos (para partilhar modelos), o que ativaria o target Tailwind. Como a API não tem interface web, não precisa de CSS, e o Node.js não está disponível neste Dockerfile.

10.2 Deployment na Render.com

A plataforma está publicada na Render.com, um serviço cloud que oferece um plano gratuito para containers Docker. A infraestrutura é definida declarativamente no ficheiro render.yaml (Render Blueprint), que descreve os dois serviços e as suas variáveis de ambiente:

services:
  - type: web
    name: webservicos
    env: docker
    dockerfilePath: ./Dockerfile
    plan: free
    envVars:
      - key: ASPNETCORE_ENVIRONMENT
        value: Production
      - key: ConnectionStrings__DefaultConnection
        value: Data Source=/app/webservicos.db

  - type: web
    name: webservicos-api
    env: docker
    dockerfilePath: ./Dockerfile.api
    plan: free
    envVars:
      - key: ASPNETCORE_ENVIRONMENT
        value: Production
      - key: ConnectionStrings__DefaultConnection
        value: Data Source=/app/webservicos.db

Os URLs de produção são:

Nota sobre SQLite em cloud: No plano gratuito da Render, o filesystem não é persistente entre deploys. Cada novo deploy começa com uma base de dados vazia, preenchida pelo seed automático. Para persistência real seria necessário configurar um volume ou migrar para PostgreSQL.

10.3 Pipeline de Entrega Contínua

O projeto usa dois repositórios GitHub com auto-deploy configurado:

Remote GitRepositórioBranchFinalidade
originkapa1233/projetoDWmasterRepositório principal do grupo
diogodiogogodinhowaze/projetodwmainLigado à Render para auto-deploy

Sempre que é feito git push diogo master:main, a Render deteta a alteração e inicia automaticamente um novo build Docker e deploy dos dois serviços. O processo completo de build (inclui compilação .NET + npm build do Tailwind) demora tipicamente 3 a 5 minutos.

11. Tecnologias e Dependências

TecnologiaVersãoPapel no Projeto
ASP.NET Core8.0Framework base — Razor Pages e MVC Controllers
Entity Framework Core8.0ORM para acesso à base de dados e migrations
SQLiteMotor de base de dados (ficheiro único, sem servidor)
ASP.NET Core Identity8.0Autenticação, autorização, gestão de roles
ASP.NET Core SignalR8.0Comunicação em tempo real (WebSockets)
Swashbuckle.AspNetCore6.5.0Geração automática de documentação OpenAPI/Swagger
Bootstrap5.3.2Framework CSS — grid, componentes, utilitários
Bootstrap Icons1.11.3Biblioteca de ícones SVG inline
jQuery3.7.1Manipulação DOM, validação de formulários
Tailwind CSS3.4.xUtilitários CSS customizados (paleta dark, gradientes)
AOS (Animate On Scroll)2.3.xAnimações de entrada ao scroll
Microsoft SignalR JS8.0.0Cliente JavaScript do SignalR
Node.js20.xBuild tool para compilar Tailwind CSS (apenas build)
DockerContentorização para deployment
Render.comPlataforma cloud para hosting (plano gratuito)
GitHubControlo de versão + trigger de auto-deploy
Microsoft.Extensions.Hosting.WindowsServices8.0Suporte para execução como Windows Service (desenvolvimento local)

12. Padrões de Desenvolvimento

12.1 Padrões Arquiteturais

12.2 Padrões de Dados

12.3 Padrões de UI

13. Dificuldades e Soluções

Durante o desenvolvimento e deployment do projeto, surgiram vários problemas técnicos que exigiram pesquisa e resolução criativa. Documentam-se aqui os mais relevantes:

ProblemaCausaSolução
npm: not found durante Docker build O target MSBuild BuildTailwind executa npm run build, mas a imagem Docker base (.NET SDK) não inclui Node.js Adicionar instalação de Node.js 20.x via NodeSource no Dockerfile da app web
npm ci falha por falta de package-lock.json O ficheiro package-lock.json está no .gitignore e não é enviado para o repositório Substituir npm ci por npm install, que não requer o lock file
API falha no build Docker por npm: not found A API referencia o projeto WebServicos, ativando o target Tailwind mesmo não precisando de CSS Adicionar condição SkipNpmBuild=true ao target e passar o flag no dotnet publish da API
SQLite Error 14: unable to open database O caminho relativo ../WebServicos/webservicos.db resolvia para /WebServicos/webservicos.db no container Docker (sem permissão) Sobrescrever a connection string nas env vars do render.yaml para Data Source=/app/webservicos.db
Render Blueprint "Resources already up to date" Um serviço Node.js antigo existia no dashboard e bloqueava a criação dos serviços Docker Eliminar o serviço obsoleto e sincronizar novamente o Blueprint
Render ligado ao repositório errado O repositório principal do grupo (kapa1233/projetoDW) estava configurado na Render, mas o utilizador da Render era diogogodinhowaze Adicionar segundo remote Git (diogo) e fazer force push inicial para o repositório correto
SQLite perde dados a cada deploy/reinício na Render O plano gratuito da Render não tem disco persistente — o filesystem do container é destruído a cada deploy ou quando o serviço adormece Migrar para PostgreSQL gerido pela Render (plano free): adicionar secção databases: ao render.yaml com fromDatabase.connectionString nos dois serviços
Npgsql rejeita o formato postgresql:// do DATABASE_URL A Render injeta a connection string no formato URL (postgresql://user:pass@host:port/db) mas o NpgsqlConnectionStringBuilder só aceita o formato ADO.NET (Host=...;Database=...;) Criar função ConvertDatabaseUrl() em ambos os Program.cs que converte o URL para formato ADO.NET antes de passar ao UseNpgsql()
Erro "timestamp with time zone literal cannot be generated for Unspecified DateTime" na migration O Npgsql 8 usa timestamp with time zone por defeito, que exige DateTimeKind.Utc em todos os valores — incluindo os dados de seed (DataCriacao dos serviços) guardados na migration com Kind = Unspecified Ativar AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true) em ambos os Program.cs e na ApplicationDbContextFactory; regenerar as migrations com o switch ativo, que passa as colunas para timestamp without time zone sem restrição de UTC

14. Melhorias e Evolução da Plataforma

Ao longo do desenvolvimento, a plataforma passou por melhorias concretas que a aproximam de um produto real. Esta secção documenta as evoluções mais significativas face à versão inicial.

14.1 Persistência de Dados com PostgreSQL

A alteração mais impactante foi a migração de SQLite para PostgreSQL como base de dados de produção. Esta mudança resolve um problema fundamental que existia na versão inicial da plataforma publicada na Render.com.

O problema com SQLite na cloud

Na versão original, a base de dados era um ficheiro SQLite (webservicos.db) armazenado no sistema de ficheiros do container Docker. O plano gratuito da Render não fornece disco persistente, o que significa que:

Exemplo concreto: Um utilizador registava-se, submetia um pedido e trocava mensagens com o admin. No dia seguinte, ao tentar fazer login, a conta já não existia — porque entretanto houve um novo deploy ou o container reiniciou por inatividade.

A solução: PostgreSQL gerido pela Render

A solução foi integrar uma base de dados PostgreSQL gerida pela própria Render, independente dos containers das aplicações. O PostgreSQL é um serviço separado com disco persistente próprio — os dados vivem fora dos containers e sobrevivem a qualquer número de deploys, reinícios ou hibernações.

Antes (SQLite)Depois (PostgreSQL)
Onde vivem os dadosFicheiro no container DockerServidor PostgreSQL dedicado na Render
Utilizadores registadosApagados a cada deploy/reinícioPersistem para sempre
Pedidos e mensagensPerdidos com o containerGuardados permanentemente
Após hibernação (15 min)Base de dados reiniciada do zeroDados intactos — só o container acorda
Após novo deployTudo apagadoApenas o schema é atualizado (migrations)
Partilha entre serviçosImpossível (ficheiro local)App web e API acedem à mesma BD

Implementação técnica

A integração foi feita de forma a não quebrar o desenvolvimento local: a aplicação deteta automaticamente se está em produção (presença da variável DATABASE_URL injetada pela Render) e escolhe o provider adequado:

var databaseUrl = Environment.GetEnvironmentVariable("DATABASE_URL");
if (databaseUrl != null)
{
    // Produção: PostgreSQL gerido pela Render
    var npgsqlConn = ConvertDatabaseUrl(databaseUrl);
    options.UseNpgsql(npgsqlConn);
}
else
{
    // Local: SQLite — sem necessidade de instalar PostgreSQL
    options.UseSqlite(sqliteConnStr);
}

O ficheiro render.yaml foi atualizado para declarar a base de dados e ligar automaticamente os dois serviços (app web e API) à mesma instância PostgreSQL:

databases:
  - name: webservicos-db
    plan: free

services:
  - name: webservicos
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: webservicos-db
          property: connectionString

A gestão do schema é feita pelo EF Core Migrations: no arranque em produção, db.Database.Migrate() aplica automaticamente quaisquer alterações de schema pendentes, garantindo que a base de dados está sempre sincronizada com o código sem intervenção manual.

14.2 Outras Melhorias de Produto

MelhoriaDescriçãoImpacto
Página "Sobre" redesenhada Substituição da tabela estática por layout com hero, diagrama de fluxo, cards de autores com avatares, badges de tecnologias e secção GitHub Mais apelativa para avaliação académica; auto-explicativa para visitantes
Relatório técnico descarregável Botão "Descarregar Relatório" na página Sobre serve o HTML diretamente de wwwroot/ Documentação acessível a partir da própria plataforma
Comentários XML em todo o código Adição de /// <summary> em todas as classes, métodos e propriedades; comentários inline na lógica não óbvia Código legível no IntelliSense; facilita a avaliação e manutenção
Animação de partículas na homepage Canvas HTML5 interativo com partículas que formam conexões ao aproximar o rato Estética tech moderna, diferenciadora visualmente
Deploy automático (CI/CD) Qualquer git push para o repositório desencadeia automaticamente um novo build e deploy na Render Zero intervenção manual para publicar novas versões

15. Conclusão

O projeto KapaDW concretizou com sucesso os objetivos definidos para a unidade curricular de Desenvolvimento Web. A plataforma implementada vai além dos requisitos mínimos de um CRUD, incorporando funcionalidades características de sistemas web profissionais: comunicação em tempo real, workflows de aprovação, documentação de API, e deployment em cloud com CI/CD.

Do ponto de vista técnico, o projeto demonstrou competências em múltiplas camadas:

As maiores aprendizagens técnicas foram na área de contentorização com Docker, onde a necessidade de incluir Node.js na imagem de build (para compilar Tailwind CSS num ambiente .NET) e de gerir caminhos de base de dados absolutos no container exigiram um diagnóstico cuidado dos logs de erro e compreensão do comportamento do sistema de ficheiros em containers.

Resultado: A plataforma está publicada e acessível 24/7 em https://webservicos.onrender.com, com deploy automático a cada push para o repositório GitHub, demonstrando um fluxo de desenvolvimento moderno e profissional.

16. Referências



KapaDW — Relatório Técnico  |  Diogo Godinho (27220) & Kanstantsin Khomchanka (27230)  |  IPT 2024/2025